Merge branch 'main' into chat-improvements
2
.github/FUNDING.yml
vendored
|
|
@ -1,2 +1,2 @@
|
|||
github: tommyxchow
|
||||
custom: ["buymeacoffee.com/tommychow", "paypal.com/donate/?business=NF33JDG6KBU6W"]
|
||||
custom: buymeacoffee.com/tommychow
|
||||
|
|
|
|||
123
.gitignore
vendored
|
|
@ -1,63 +1,62 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Folder containing random testing files
|
||||
random/
|
||||
|
||||
# fastlane specific
|
||||
**/fastlane/report.xml
|
||||
|
||||
# deliver temporary files
|
||||
**/fastlane/Preview.html
|
||||
|
||||
# snapshot generated screenshots
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/metadata/android/en-US/images
|
||||
|
||||
# scan temporary files
|
||||
**/fastlane/test_output
|
||||
|
||||
# potentially sensitive review information
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Folder containing random testing files
|
||||
random/
|
||||
|
||||
# fastlane specific
|
||||
**/fastlane/report.xml
|
||||
|
||||
# deliver temporary files
|
||||
**/fastlane/Preview.html
|
||||
|
||||
# snapshot generated screenshots
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/metadata/android/en-US/images
|
||||
|
||||
# scan temporary files
|
||||
**/fastlane/test_output
|
||||
|
||||
# potentially sensitive review information
|
||||
**/fastlane/metadata/review_information
|
||||
|
|
@ -98,9 +98,8 @@ For a more detailed overview, visit [frostyapp.io](https://www.frostyapp.io/).
|
|||
|
||||
If you appreciate my work and would like to donate/tip, you can through:
|
||||
|
||||
- [Buy Me a Coffee](https://www.buymeacoffee.com/tommychow)
|
||||
- [GitHub Sponsors](https://github.com/sponsors/tommyxchow)
|
||||
- [PayPal](https://www.paypal.com/donate/?business=NF33JDG6KBU6W)
|
||||
- [Buy Me a Coffee](https://www.buymeacoffee.com/tommychow)
|
||||
|
||||
Otherwise, downloading Frosty, leaving a review, or starring this repository is more than enough to show support. Thank you!
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +1,38 @@
|
|||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/**.g.dart
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
- directives_ordering
|
||||
- prefer_single_quotes
|
||||
- always_use_package_imports
|
||||
- avoid_void_async
|
||||
- prefer_final_locals
|
||||
- prefer_final_in_for_each
|
||||
- avoid_redundant_argument_values
|
||||
- unnecessary_parenthesis
|
||||
- always_declare_return_types
|
||||
- require_trailing_commas
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/**.g.dart
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
- directives_ordering
|
||||
- prefer_single_quotes
|
||||
- always_use_package_imports
|
||||
- avoid_void_async
|
||||
- prefer_final_locals
|
||||
- prefer_final_in_for_each
|
||||
- avoid_redundant_argument_values
|
||||
- unnecessary_parenthesis
|
||||
- always_declare_return_types
|
||||
- require_trailing_commas
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
|
|||
2
android/.gitignore
vendored
|
|
@ -7,7 +7,7 @@ gradle-wrapper.jar
|
|||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.6)
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
artifactory (3.0.15)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.854.0)
|
||||
aws-sdk-core (3.187.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.963.0)
|
||||
aws-sdk-core (3.201.4)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.72.0)
|
||||
aws-sdk-core (~> 3, >= 3.184.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.137.0)
|
||||
aws-sdk-core (~> 3, >= 3.181.0)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.157.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
|
|
@ -36,7 +39,7 @@ GEM
|
|||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.104.0)
|
||||
excon (0.109.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
|
|
@ -58,22 +61,22 @@ GEM
|
|||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.7)
|
||||
fastlane (2.217.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.222.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
|
|
@ -85,6 +88,7 @@ GEM
|
|||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
|
|
@ -93,10 +97,10 @@ GEM
|
|||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
|
|
@ -105,11 +109,11 @@ GEM
|
|||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.53.0)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.2)
|
||||
google-apis-core (0.11.3)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
|
|
@ -117,15 +121,14 @@ GEM
|
|||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.29.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-core (1.6.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
|
|
@ -145,33 +148,36 @@ GEM
|
|||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.5)
|
||||
http-cookie (1.0.6)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.7.1)
|
||||
mini_magick (4.12.0)
|
||||
json (2.7.2)
|
||||
jwt (2.8.2)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.3.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
optparse (0.1.1)
|
||||
nkf (0.2.0)
|
||||
optparse (0.5.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.0)
|
||||
public_suffix (5.0.4)
|
||||
rake (13.1.0)
|
||||
plist (3.7.1)
|
||||
public_suffix (5.1.1)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.6)
|
||||
rexml (3.3.5)
|
||||
strscan
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
security (0.1.5)
|
||||
signet (0.18.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
|
|
@ -180,12 +186,13 @@ GEM
|
|||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
|
|
@ -193,15 +200,14 @@ GEM
|
|||
unf_ext
|
||||
unf_ext (0.0.9.1)
|
||||
unicode-display_width (2.5.0)
|
||||
webrick (1.8.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.23.0)
|
||||
xcodeproj (1.25.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
rexml (>= 3.3.2, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
|
|
|
|||
|
|
@ -1,86 +1,60 @@
|
|||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
// START: FlutterFire Configuration
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.firebase-perf'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
// END: FlutterFire Configuration
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "com.tommychow.frosty"
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.tommychow.frosty"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
// START: FlutterFire Configuration
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.firebase-perf'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
// END: FlutterFire Configuration
|
||||
id "kotlin-android"
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tommychow.frosty"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.tommychow.frosty"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties["keyAlias"]
|
||||
keyPassword keystoreProperties["keyPassword"]
|
||||
storeFile keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null
|
||||
storePassword keystoreProperties["storePassword"]
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,57 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="Frosty"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="false">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="Frosty"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="false">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.2 KiB |
|
|
@ -1,36 +1,18 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.9.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
// START: FlutterFire Configuration
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
classpath 'com.google.firebase:perf-plugin:1.4.2'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5'
|
||||
// END: FlutterFire Configuration
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = "../build"
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
|
|||
10
android/fastlane/metadata/android/en-US/changelogs/48.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
New Features
|
||||
• Added the ability to tap on user mentions in chat (by @Artiu on GitHub)
|
||||
• Added stream latency details to the custom overlay in landscape mode (by @Artiu on GitHub)
|
||||
|
||||
Improvements
|
||||
• Improved how the default Twitch overlay is hidden when the custom overlay is enabled (by @Artiu on GitHub)
|
||||
• Links in chat are now also underlined
|
||||
|
||||
Bug Fixes
|
||||
• Fixed 7TV emote set updates not respecting the chat delay setting
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Bug Fixes
|
||||
• Fixed stream quality options not showing (by @Shingyx on GitHub)
|
||||
• Fixed stream latency not showing (by @Shingyx on GitHub)
|
||||
• Fixed stream latency icon color on light mode
|
||||
|
|
@ -0,0 +1 @@
|
|||
This update contains a variety of new features including a quick toggle to enter or exit chat-only mode, the ability to zoom stream thumbnails and emotes or badges, an option to show historical recent messages in chat, and other improvements and bug fixes.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
New Features
|
||||
• Added an option to un-darken historical recent messages in chat
|
||||
|
||||
Improvements
|
||||
• Moved exact matches of searched categories to the top (by @Shingyx on GitHub)
|
||||
|
||||
Bug Fixes
|
||||
• Fixed extraneous notices when toggling chat-only mode
|
||||
• Fixed cookie banner blocking login and added backup workaround instructions in the login screen toolbar (instructions by @UmmonTeal on GitHub)
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
|
|||
10
android/gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||
|
|
|
|||
|
|
@ -1,11 +1,28 @@
|
|||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
|
||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||
id "com.google.firebase.firebase-perf" version "1.4.2" apply false
|
||||
id "com.google.firebase.crashlytics" version "2.9.9" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
|
|
|||
BIN
assets/fonts/Inter-BlackItalic.ttf
Normal file
BIN
assets/fonts/Inter-BoldItalic.ttf
Normal file
BIN
assets/fonts/Inter-ExtraBoldItalic.ttf
Normal file
BIN
assets/fonts/Inter-ExtraLightItalic.ttf
Normal file
BIN
assets/fonts/Inter-Italic.ttf
Normal file
BIN
assets/fonts/Inter-LightItalic.ttf
Normal file
BIN
assets/fonts/Inter-MediumItalic.ttf
Normal file
BIN
assets/fonts/Inter-SemiBoldItalic.ttf
Normal file
BIN
assets/fonts/Inter-ThinItalic.ttf
Normal file
45
assets/release-notes.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
## v4.5.0
|
||||
|
||||
August 16, 2024
|
||||
|
||||
### New Features
|
||||
|
||||
- Added an option to un-darken historical recent messages in chat
|
||||
|
||||
### Improvements
|
||||
|
||||
- [iPadOS] Enabled video quality settings on iPad (by @Shingyx on GitHub)
|
||||
- Moved exact matches of searched categories to the top (by @Shingyx on GitHub)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed extraneous notices when toggling chat-only mode
|
||||
- Fixed cookie banner blocking login and added backup workaround instructions in the login screen toolbar (instructions by @UmmonTeal on GitHub)
|
||||
|
||||
---
|
||||
|
||||
## v4.4.0
|
||||
|
||||
August 13, 2024
|
||||
|
||||
### New Features
|
||||
|
||||
- Added a quick toggle in the chat details menu to enter or exit chat-only mode
|
||||
- Added an experimental option to sync message delay with stream latency
|
||||
- Added options to individually disable emotes or badges from Twitch, 7TV, BTTV, and FFZ
|
||||
- Added the ability to view and zoom emotes or badges when tapping on them
|
||||
- Added the ability to view and zoom stream thumbnails when long-pressing on it
|
||||
- Added an option to show historical recent messages in chat
|
||||
- Added latest release notes on launch
|
||||
|
||||
### Improvements
|
||||
|
||||
- [Android] In-app links will now prefer Android Custom Tabs
|
||||
- Decreased the default landscape chat width from 30% to 25%
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed animated FFZ emotes not animating
|
||||
- Fixed rare case where the custom video overlay doesn't enter the playing state
|
||||
|
||||
#### See all release notes at https://github.com/tommyxchow/frosty/releases
|
||||
22
firebase.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"flutter": {
|
||||
"platforms": {
|
||||
"android": {
|
||||
"default": {
|
||||
"projectId": "frosty-128de",
|
||||
"appId": "1:403282332389:android:a7ff237168bad4c30be20c",
|
||||
"fileOutput": "android/app/google-services.json"
|
||||
}
|
||||
},
|
||||
"dart": {
|
||||
"lib/firebase_options.dart": {
|
||||
"projectId": "frosty-128de",
|
||||
"configurations": {
|
||||
"android": "1:403282332389:android:a7ff237168bad4c30be20c",
|
||||
"ios": "1:403282332389:ios:0fc3f8c3d4e80c8b0be20c"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,6 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.6)
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
artifactory (3.0.15)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.854.0)
|
||||
aws-sdk-core (3.187.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.963.0)
|
||||
aws-sdk-core (3.201.4)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.72.0)
|
||||
aws-sdk-core (~> 3, >= 3.184.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.137.0)
|
||||
aws-sdk-core (~> 3, >= 3.181.0)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.157.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
|
|
@ -36,7 +39,7 @@ GEM
|
|||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.104.0)
|
||||
excon (0.109.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
|
|
@ -58,22 +61,22 @@ GEM
|
|||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.7)
|
||||
fastlane (2.217.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.222.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
|
|
@ -85,6 +88,7 @@ GEM
|
|||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
|
|
@ -93,10 +97,10 @@ GEM
|
|||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
|
|
@ -105,11 +109,11 @@ GEM
|
|||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.53.0)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.2)
|
||||
google-apis-core (0.11.3)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
|
|
@ -117,15 +121,14 @@ GEM
|
|||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.29.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-core (1.6.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
|
|
@ -145,33 +148,36 @@ GEM
|
|||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.5)
|
||||
http-cookie (1.0.6)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.7.1)
|
||||
mini_magick (4.12.0)
|
||||
json (2.7.2)
|
||||
jwt (2.8.2)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.3.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
optparse (0.1.1)
|
||||
nkf (0.2.0)
|
||||
optparse (0.5.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.0)
|
||||
public_suffix (5.0.4)
|
||||
rake (13.1.0)
|
||||
plist (3.7.1)
|
||||
public_suffix (5.1.1)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.6)
|
||||
rexml (3.3.5)
|
||||
strscan
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
security (0.1.5)
|
||||
signet (0.18.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
|
|
@ -180,12 +186,13 @@ GEM
|
|||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
|
|
@ -193,15 +200,14 @@ GEM
|
|||
unf_ext
|
||||
unf_ext (0.0.9.1)
|
||||
unicode-display_width (2.5.0)
|
||||
webrick (1.8.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.23.0)
|
||||
xcodeproj (1.25.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
rexml (>= 3.3.2, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
# platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
|
|
|||
134
ios/Podfile.lock
|
|
@ -16,22 +16,22 @@ PODS:
|
|||
- Firebase/Performance (10.18.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebasePerformance (~> 10.18.0)
|
||||
- firebase_analytics (10.7.2):
|
||||
- firebase_analytics (10.8.0):
|
||||
- Firebase/Analytics (= 10.18.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (2.24.0):
|
||||
- firebase_core (2.24.2):
|
||||
- Firebase/CoreOnly (= 10.18.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (3.4.6):
|
||||
- firebase_crashlytics (3.4.9):
|
||||
- Firebase/Crashlytics (= 10.18.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_performance (0.9.3-6):
|
||||
- firebase_performance (0.9.3-8):
|
||||
- Firebase/Performance (= 10.18.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseABTesting (10.18.0):
|
||||
- FirebaseABTesting (10.29.0):
|
||||
- FirebaseCore (~> 10.0)
|
||||
- FirebaseAnalytics (10.18.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 10.18.0)
|
||||
|
|
@ -55,9 +55,9 @@ PODS:
|
|||
- FirebaseCoreInternal (~> 10.0)
|
||||
- GoogleUtilities/Environment (~> 7.12)
|
||||
- GoogleUtilities/Logger (~> 7.12)
|
||||
- FirebaseCoreExtension (10.18.0):
|
||||
- FirebaseCoreExtension (10.29.0):
|
||||
- FirebaseCore (~> 10.0)
|
||||
- FirebaseCoreInternal (10.18.0):
|
||||
- FirebaseCoreInternal (10.29.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.8)"
|
||||
- FirebaseCrashlytics (10.18.0):
|
||||
- FirebaseCore (~> 10.5)
|
||||
|
|
@ -67,7 +67,7 @@ PODS:
|
|||
- GoogleUtilities/Environment (~> 7.8)
|
||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
||||
- PromisesObjC (~> 2.1)
|
||||
- FirebaseInstallations (10.18.0):
|
||||
- FirebaseInstallations (10.29.0):
|
||||
- FirebaseCore (~> 10.0)
|
||||
- GoogleUtilities/Environment (~> 7.8)
|
||||
- GoogleUtilities/UserDefaults (~> 7.8)
|
||||
|
|
@ -82,30 +82,30 @@ PODS:
|
|||
- GoogleUtilities/ISASwizzler (~> 7.8)
|
||||
- GoogleUtilities/MethodSwizzler (~> 7.8)
|
||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
||||
- FirebaseRemoteConfig (10.18.0):
|
||||
- FirebaseRemoteConfig (10.29.0):
|
||||
- FirebaseABTesting (~> 10.0)
|
||||
- FirebaseCore (~> 10.0)
|
||||
- FirebaseInstallations (~> 10.0)
|
||||
- FirebaseRemoteConfigInterop (~> 10.23)
|
||||
- FirebaseSharedSwift (~> 10.0)
|
||||
- GoogleUtilities/Environment (~> 7.8)
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.8)"
|
||||
- FirebaseSessions (10.18.0):
|
||||
- FirebaseRemoteConfigInterop (10.29.0)
|
||||
- FirebaseSessions (10.29.0):
|
||||
- FirebaseCore (~> 10.5)
|
||||
- FirebaseCoreExtension (~> 10.0)
|
||||
- FirebaseInstallations (~> 10.0)
|
||||
- GoogleDataTransport (~> 9.2)
|
||||
- GoogleUtilities/Environment (~> 7.10)
|
||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
||||
- GoogleUtilities/Environment (~> 7.13)
|
||||
- GoogleUtilities/UserDefaults (~> 7.13)
|
||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||
- PromisesSwift (~> 2.1)
|
||||
- FirebaseSharedSwift (10.18.0)
|
||||
- FirebaseSharedSwift (10.29.0)
|
||||
- Flutter (1.0.0)
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- GoogleAppMeasurement (10.18.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 10.18.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
|
||||
|
|
@ -126,30 +126,40 @@ PODS:
|
|||
- GoogleUtilities/Network (~> 7.11)
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.11)"
|
||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
||||
- GoogleDataTransport (9.3.0):
|
||||
- GoogleDataTransport (9.4.1):
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleUtilities/AppDelegateSwizzler (7.12.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (7.13.3):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network
|
||||
- GoogleUtilities/Environment (7.12.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Environment (7.13.3):
|
||||
- GoogleUtilities/Privacy
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleUtilities/ISASwizzler (7.12.0)
|
||||
- GoogleUtilities/Logger (7.12.0):
|
||||
- GoogleUtilities/ISASwizzler (7.13.3):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (7.13.3):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/MethodSwizzler (7.12.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/MethodSwizzler (7.13.3):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network (7.12.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Network (7.13.3):
|
||||
- GoogleUtilities/Logger
|
||||
- "GoogleUtilities/NSData+zlib"
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Reachability
|
||||
- "GoogleUtilities/NSData+zlib (7.12.0)"
|
||||
- GoogleUtilities/Reachability (7.12.0):
|
||||
- "GoogleUtilities/NSData+zlib (7.13.3)":
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (7.13.3)
|
||||
- GoogleUtilities/Reachability (7.13.3):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/UserDefaults (7.12.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/UserDefaults (7.13.3):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- in_app_review (0.2.0):
|
||||
- Flutter
|
||||
- nanopb (2.30909.1):
|
||||
|
|
@ -162,15 +172,15 @@ PODS:
|
|||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.3.1)
|
||||
- PromisesSwift (2.3.1):
|
||||
- PromisesObjC (= 2.3.1)
|
||||
- PromisesObjC (2.4.0)
|
||||
- PromisesSwift (2.4.0):
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
|
|
@ -192,7 +202,7 @@ DEPENDENCIES:
|
|||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
|
@ -209,9 +219,9 @@ SPEC REPOS:
|
|||
- FirebaseInstallations
|
||||
- FirebasePerformance
|
||||
- FirebaseRemoteConfig
|
||||
- FirebaseRemoteConfigInterop
|
||||
- FirebaseSessions
|
||||
- FirebaseSharedSwift
|
||||
- FMDB
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
|
|
@ -247,7 +257,7 @@ EXTERNAL SOURCES:
|
|||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
wakelock_plus:
|
||||
|
|
@ -257,42 +267,42 @@ EXTERNAL SOURCES:
|
|||
|
||||
SPEC CHECKSUMS:
|
||||
advanced_in_app_review: fe8fd50b8670cb3dbe67e4c26ca643f7aea8dbb8
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06
|
||||
firebase_analytics: a8c0d8ba83f213c9b775b5f61d36dadaafb1b239
|
||||
firebase_core: f802c5c1f6caff9b8d38b591a36e7b25f8878936
|
||||
firebase_crashlytics: 0db78a5b6badc630f21a833d0c9ab20ab5d81948
|
||||
firebase_performance: e24d96d0be9d67e1adc1a1fdff6e78c2c460b4e6
|
||||
FirebaseABTesting: d8b10ff4c6d1a9d6b11f02a08463ad5fd9fc6b1b
|
||||
firebase_analytics: b3d6dd14c61549a29abb10e9843446a4d8bb53eb
|
||||
firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5
|
||||
firebase_crashlytics: 4b91b8ad60ee7c168fe88979f84c9573a729de7a
|
||||
firebase_performance: 2183122a3c7a650c80d8c164e9e28f13c4c62fc7
|
||||
FirebaseABTesting: d87f56707159bae64e269757a6e963d490f2eebe
|
||||
FirebaseAnalytics: 4d310b35c48eaa4a058ddc04bdca6bdb5dc0fe80
|
||||
FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f
|
||||
FirebaseCoreExtension: 62b201498aa10535801cdf3448c7f4db5e24ed80
|
||||
FirebaseCoreInternal: 8eb002e564b533bdcf1ba011f33f2b5c10e2ed4a
|
||||
FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f
|
||||
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
|
||||
FirebaseCrashlytics: 86d5bce01f42fa1db265f87ff1d591f04db610ec
|
||||
FirebaseInstallations: e842042ec6ac1fd2e37d7706363ebe7f662afea4
|
||||
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
|
||||
FirebasePerformance: c406a9198d8aabfbac281b42855f5122fc1bcf69
|
||||
FirebaseRemoteConfig: bbd42790a4e84fde6aab7eae810b608e7b5c0bf6
|
||||
FirebaseSessions: f90fe9212ee2818641eda051c0835c9c4e30d9ae
|
||||
FirebaseSharedSwift: 62e248642c0582324d0390706cadd314687c116b
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
FirebaseRemoteConfig: 48ef3f243742a8d72422ccfc9f986e19d7de53fd
|
||||
FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d
|
||||
FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc
|
||||
FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
GoogleAppMeasurement: 70ce9aa438cff1cfb31ea3e660bcc67734cb716e
|
||||
GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe
|
||||
GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
|
||||
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
|
||||
|
||||
COCOAPODS: 1.13.0
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@
|
|||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
|
|
@ -367,7 +367,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
|
@ -448,7 +448,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
|
@ -497,7 +497,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import UIKit
|
||||
import Flutter
|
||||
|
||||
@UIApplicationMain
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.2 KiB |
|
|
@ -48,13 +48,11 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2023 Tommy Chow
|
||||
2024 Tommy Chow
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
New Features
|
||||
• Added an option to un-darken historical recent messages in chat
|
||||
|
||||
Improvements
|
||||
• [iPadOS] Enabled video quality settings on iPad (by @Shingyx on GitHub)
|
||||
• Moved exact matches of searched categories to the top (by @Shingyx on GitHub)
|
||||
|
||||
Bug Fixes
|
||||
• Fixed own display name not showing in sent chat messages
|
||||
• Fixed rare case where 7TV emotes don't show in chat
|
||||
• Fixed extraneous notices when toggling chat-only mode
|
||||
• Fixed cookie banner blocking login and added backup workaround instructions in the login screen toolbar (instructions by @UmmonTeal on GitHub)
|
||||
|
|
@ -522,4 +522,20 @@ class TwitchApi {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Unblocks the user with the given ID and returns true on success or false on failure.
|
||||
Future<List<dynamic>> getRecentMessages({
|
||||
required String userLogin,
|
||||
}) async {
|
||||
final url = Uri.parse(
|
||||
'https://recent-messages.robotty.de/api/v2/recent-messages/$userLogin',
|
||||
);
|
||||
|
||||
final response = await _client.get(url);
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body)['messages'] as List;
|
||||
} else {
|
||||
return Future.error('Failed to get recent messages for $userLogin');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import 'package:frosty/screens/onboarding/onboarding_intro.dart';
|
|||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/theme.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -25,6 +26,19 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
||||
// Pass all uncaught "fatal" errors from the framework to Crashlytics
|
||||
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
|
||||
|
||||
// Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final firstRun = prefs.getBool('first_run') ?? true;
|
||||
|
|
@ -38,6 +52,8 @@ void main() async {
|
|||
await storage.deleteAll();
|
||||
}
|
||||
|
||||
await initUtils();
|
||||
|
||||
// With the shared preferences instance, obtain the existing user settings if it exists.
|
||||
// If default settings don't exist, use an empty JSON string to use the default values.
|
||||
final userSettings = prefs.getString('settings') ?? '{}';
|
||||
|
|
@ -48,19 +64,6 @@ void main() async {
|
|||
// Create a MobX reaction that will save the settings on disk every time they are changed.
|
||||
autorun((_) => prefs.setString('settings', jsonEncode(settingsStore)));
|
||||
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
||||
// Pass all uncaught "fatal" errors from the framework to Crashlytics
|
||||
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
|
||||
|
||||
// Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
|
||||
/// Initialize API services with a common client.
|
||||
/// This will prevent every request from creating a new client instance.
|
||||
final client = Client();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ BadgeInfoTwitch _$BadgeInfoTwitchFromJson(Map<String, dynamic> json) =>
|
|||
);
|
||||
|
||||
BadgeInfoFFZ _$BadgeInfoFFZFromJson(Map<String, dynamic> json) => BadgeInfoFFZ(
|
||||
json['id'] as int,
|
||||
(json['id'] as num).toInt(),
|
||||
json['title'] as String,
|
||||
json['color'] as String,
|
||||
BadgeUrlsFFZ.fromJson(json['urls'] as Map<String, dynamic>),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'chatters.dart';
|
|||
// **************************************************************************
|
||||
|
||||
ChatUsers _$ChatUsersFromJson(Map<String, dynamic> json) => ChatUsers(
|
||||
json['chatter_count'] as int,
|
||||
(json['chatter_count'] as num).toInt(),
|
||||
Chatters.fromJson(json['chatters'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ class EmoteFFZ {
|
|||
final int width;
|
||||
final OwnerFFZ owner;
|
||||
final ImagesFFZ urls;
|
||||
final ImagesFFZ? animated;
|
||||
|
||||
const EmoteFFZ(
|
||||
this.name,
|
||||
|
|
@ -115,6 +116,7 @@ class EmoteFFZ {
|
|||
this.width,
|
||||
this.owner,
|
||||
this.urls,
|
||||
this.animated,
|
||||
);
|
||||
|
||||
factory EmoteFFZ.fromJson(Map<String, dynamic> json) =>
|
||||
|
|
@ -251,7 +253,12 @@ class Emote {
|
|||
zeroWidth: false,
|
||||
width: emote.width,
|
||||
height: emote.height,
|
||||
url: emote.urls.url4x ?? emote.urls.url2x ?? emote.urls.url1x,
|
||||
url: emote.animated?.url4x ??
|
||||
emote.animated?.url2x ??
|
||||
emote.animated?.url1x ??
|
||||
emote.urls.url4x ??
|
||||
emote.urls.url2x ??
|
||||
emote.urls.url1x,
|
||||
type: type,
|
||||
ownerDisplayName: emote.owner.displayName,
|
||||
ownerUsername: emote.owner.name,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ EmoteBTTVChannel _$EmoteBTTVChannelFromJson(Map<String, dynamic> json) =>
|
|||
);
|
||||
|
||||
RoomFFZ _$RoomFFZFromJson(Map<String, dynamic> json) => RoomFFZ(
|
||||
json['set'] as int,
|
||||
(json['set'] as num).toInt(),
|
||||
json['vip_badge'] == null
|
||||
? null
|
||||
: ImagesFFZ.fromJson(json['vip_badge'] as Map<String, dynamic>),
|
||||
|
|
@ -51,10 +51,13 @@ OwnerFFZ _$OwnerFFZFromJson(Map<String, dynamic> json) => OwnerFFZ(
|
|||
|
||||
EmoteFFZ _$EmoteFFZFromJson(Map<String, dynamic> json) => EmoteFFZ(
|
||||
json['name'] as String,
|
||||
json['height'] as int,
|
||||
json['width'] as int,
|
||||
(json['height'] as num).toInt(),
|
||||
(json['width'] as num).toInt(),
|
||||
OwnerFFZ.fromJson(json['owner'] as Map<String, dynamic>),
|
||||
ImagesFFZ.fromJson(json['urls'] as Map<String, dynamic>),
|
||||
json['animated'] == null
|
||||
? null
|
||||
: ImagesFFZ.fromJson(json['animated'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Emote7TV _$Emote7TVFromJson(Map<String, dynamic> json) => Emote7TV(
|
||||
|
|
@ -73,7 +76,7 @@ Owner7TV _$Owner7TVFromJson(Map<String, dynamic> json) => Owner7TV(
|
|||
Emote7TVData _$Emote7TVDataFromJson(Map<String, dynamic> json) => Emote7TVData(
|
||||
json['id'] as String,
|
||||
json['name'] as String,
|
||||
json['flags'] as int,
|
||||
(json['flags'] as num).toInt(),
|
||||
json['owner'] == null
|
||||
? null
|
||||
: Owner7TV.fromJson(json['owner'] as Map<String, dynamic>),
|
||||
|
|
@ -89,16 +92,16 @@ Emote7TVHost _$Emote7TVHostFromJson(Map<String, dynamic> json) => Emote7TVHost(
|
|||
|
||||
Emote7TVFile _$Emote7TVFileFromJson(Map<String, dynamic> json) => Emote7TVFile(
|
||||
json['name'] as String,
|
||||
json['width'] as int,
|
||||
json['height'] as int,
|
||||
(json['width'] as num).toInt(),
|
||||
(json['height'] as num).toInt(),
|
||||
json['format'] as String,
|
||||
);
|
||||
|
||||
Emote _$EmoteFromJson(Map<String, dynamic> json) => Emote(
|
||||
name: json['name'] as String,
|
||||
realName: json['realName'] as String?,
|
||||
width: json['width'] as int?,
|
||||
height: json['height'] as int?,
|
||||
width: (json['width'] as num?)?.toInt(),
|
||||
height: (json['height'] as num?)?.toInt(),
|
||||
zeroWidth: json['zeroWidth'] as bool,
|
||||
url: json['url'] as String,
|
||||
type: $enumDecode(_$EmoteTypeEnumMap, json['type']),
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ part of 'events.dart';
|
|||
// **************************************************************************
|
||||
|
||||
SevenTVEvent _$SevenTVEventFromJson(Map<String, dynamic> json) => SevenTVEvent(
|
||||
op: json['op'] as int,
|
||||
t: json['t'] as int?,
|
||||
op: (json['op'] as num).toInt(),
|
||||
t: (json['t'] as num?)?.toInt(),
|
||||
d: SevenTVEventData.fromJson(json['d'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
|
|||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/photo_view.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
|
@ -675,7 +676,7 @@ class IRCMessage {
|
|||
Uri.parse(text),
|
||||
mode: launchExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppWebView,
|
||||
: LaunchMode.inAppBrowserView,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
|
@ -728,7 +729,13 @@ class IRCMessage {
|
|||
primary: false,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: leading,
|
||||
leading: InkWell(
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyPhotoViewDialog(imageUrl: url),
|
||||
),
|
||||
child: leading,
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
|
|
@ -764,7 +771,7 @@ class IRCMessage {
|
|||
Uri.parse(url),
|
||||
mode: launchExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppWebView,
|
||||
: LaunchMode.inAppBrowserView,
|
||||
);
|
||||
|
||||
Navigator.pop(context);
|
||||
|
|
@ -823,13 +830,16 @@ class IRCMessage {
|
|||
: splitMessage[0].substring(0, splitMessage[0].indexOf('!'));
|
||||
|
||||
// If there is an associated message, set it.
|
||||
//
|
||||
// Also remove any "INVALID/UNDEFINED" Unicode characters.
|
||||
// Rendering this character on iOS shows a question mark inside a square.
|
||||
// This character is used by some clients to bypass restrictions on repeating message.
|
||||
var message = splitMessage.length > 3
|
||||
? splitMessage.sublist(3).join(' ').substring(1)
|
||||
: null;
|
||||
String? message;
|
||||
if (splitMessage.length > 3) {
|
||||
final combinedMessage = splitMessage.sublist(3).join(' ');
|
||||
|
||||
if (combinedMessage.startsWith(':')) {
|
||||
message = combinedMessage.substring(1);
|
||||
} else {
|
||||
message = combinedMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Now process any Twitch emotes contained in the message tags.
|
||||
// The map containing emotes from the user's tags to their URL.
|
||||
|
|
@ -837,7 +847,7 @@ class IRCMessage {
|
|||
final localEmotes = <String, Emote>{};
|
||||
|
||||
final emoteTags = mappedTags['emotes'];
|
||||
if (emoteTags != null) {
|
||||
if (emoteTags != null && message != null) {
|
||||
// Emotes and their indices are separated by '/' so split them there.
|
||||
final emotes = emoteTags.split('/');
|
||||
|
||||
|
|
@ -864,7 +874,7 @@ class IRCMessage {
|
|||
final startIndex = int.parse(indexSplit[0]);
|
||||
final endIndex = int.parse(indexSplit[1]);
|
||||
|
||||
final emoteWord = message!.substring(startIndex, endIndex + 1);
|
||||
final emoteWord = message.substring(startIndex, endIndex + 1);
|
||||
|
||||
// Store the emote word and its associated URL for later use.
|
||||
localEmotes[emoteWord] = Emote(
|
||||
|
|
@ -895,6 +905,9 @@ class IRCMessage {
|
|||
// Escape the message
|
||||
message = message
|
||||
.split(' ')
|
||||
// Also remove any "INVALID/UNDEFINED" Unicode characters.
|
||||
// Rendering this character on iOS shows a question mark inside a square.
|
||||
// This character is used by some clients to bypass restrictions on repeating message.
|
||||
.map((word) => word.replaceAll('\u{E0000}', '').trim())
|
||||
.where((element) => element != '')
|
||||
.join(' ');
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ StreamTwitch _$StreamTwitchFromJson(Map<String, dynamic> json) => StreamTwitch(
|
|||
json['game_id'] as String,
|
||||
json['game_name'] as String,
|
||||
json['title'] as String,
|
||||
json['viewer_count'] as int,
|
||||
(json['viewer_count'] as num).toInt(),
|
||||
json['started_at'] as String,
|
||||
json['thumbnail_url'] as String,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class _VideoChatState extends State<VideoChat> {
|
|||
final _chatKey = GlobalKey();
|
||||
|
||||
late final ChatStore _chatStore = ChatStore(
|
||||
twitchApi: context.read<TwitchApi>(),
|
||||
channelName: widget.userLogin,
|
||||
channelId: widget.userId,
|
||||
displayName: widget.userName,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
|
@ -98,7 +97,7 @@ class Chat extends StatelessWidget {
|
|||
ChatBottomBar(chatStore: chatStore),
|
||||
PopScope(
|
||||
canPop: Platform.isIOS,
|
||||
onPopInvoked: (didPop) {
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
|
||||
// If pressing the back button on Android while the emote menu
|
||||
|
|
@ -129,26 +128,31 @@ class Chat extends StatelessWidget {
|
|||
const Divider(),
|
||||
Expanded(
|
||||
child: FrostyPageView(
|
||||
headers: const [
|
||||
headers: [
|
||||
'Recent',
|
||||
'Twitch',
|
||||
'7TV',
|
||||
'BTTV',
|
||||
'FFZ',
|
||||
if (chatStore.settings.showTwitchEmotes)
|
||||
'Twitch',
|
||||
if (chatStore.settings.show7TVEmotes) '7TV',
|
||||
if (chatStore.settings.showBTTVEmotes) 'BTTV',
|
||||
if (chatStore.settings.showFFZEmotes) 'FFZ',
|
||||
],
|
||||
children: [
|
||||
RecentEmotesPanel(
|
||||
chatStore: chatStore,
|
||||
),
|
||||
EmoteMenuPanel(
|
||||
chatStore: chatStore,
|
||||
twitchEmotes: chatStore
|
||||
.assetsStore.userEmoteSectionToEmotes,
|
||||
),
|
||||
if (chatStore.settings.showTwitchEmotes)
|
||||
EmoteMenuPanel(
|
||||
chatStore: chatStore,
|
||||
twitchEmotes: chatStore
|
||||
.assetsStore.userEmoteSectionToEmotes,
|
||||
),
|
||||
...[
|
||||
chatStore.assetsStore.sevenTVEmotes,
|
||||
chatStore.assetsStore.bttvEmotes,
|
||||
chatStore.assetsStore.ffzEmotes,
|
||||
if (chatStore.settings.show7TVEmotes)
|
||||
chatStore.assetsStore.sevenTVEmotes,
|
||||
if (chatStore.settings.showBTTVEmotes)
|
||||
chatStore.assetsStore.bttvEmotes,
|
||||
if (chatStore.settings.showFFZEmotes)
|
||||
chatStore.assetsStore.ffzEmotes,
|
||||
].map(
|
||||
(emotes) => EmoteMenuPanel(
|
||||
chatStore: chatStore,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
|
|
@ -214,6 +212,18 @@ class ChatDetails extends StatelessWidget {
|
|||
chatStore.updateNotification('Badges and emotes refreshed');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.chat_rounded),
|
||||
title: Observer(
|
||||
builder: (context) {
|
||||
return Text(
|
||||
'${chatStore.settings.showVideo ? 'Enter' : 'Exit'} chat-only mode',
|
||||
);
|
||||
},
|
||||
),
|
||||
onTap: () =>
|
||||
chatStore.settings.showVideo = !chatStore.settings.showVideo,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: const Text('Settings'),
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class _ChattersListState extends State<ChattersList> {
|
|||
.elementAt(index),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
|
|
|
|||
|
|
@ -125,91 +125,87 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
required Map<String, String> headers,
|
||||
required Function onEmoteError,
|
||||
required Function onBadgeError,
|
||||
bool showTwitchEmotes = true,
|
||||
bool showTwitchBadges = true,
|
||||
bool show7TVEmotes = true,
|
||||
bool showBTTVEmotes = true,
|
||||
bool showBTTVBadges = true,
|
||||
bool showFFZEmotes = true,
|
||||
bool showFFZBadges = true,
|
||||
}) =>
|
||||
// Fetch the global and channel's assets (emotes & badges).
|
||||
// Async awaits are placed in a list so they are performed in parallel.
|
||||
//
|
||||
// Emotes
|
||||
Future.wait([
|
||||
emotesFuture(
|
||||
channelId: channelId,
|
||||
headers: headers,
|
||||
onError: onEmoteError,
|
||||
),
|
||||
badgesFuture(
|
||||
channelId: channelId,
|
||||
headers: headers,
|
||||
onError: onBadgeError,
|
||||
),
|
||||
]);
|
||||
Future.wait([
|
||||
if (showTwitchEmotes) ...[
|
||||
twitchApi
|
||||
.getEmotesGlobal(headers: headers)
|
||||
.catchError(onEmoteError),
|
||||
twitchApi
|
||||
.getEmotesChannel(id: channelId, headers: headers)
|
||||
.then((emotes) {
|
||||
_userEmoteSectionToEmotes.update(
|
||||
'Channel Emotes',
|
||||
(existingEmoteSet) => [...existingEmoteSet, ...emotes],
|
||||
ifAbsent: () => emotes.toList(),
|
||||
);
|
||||
|
||||
@action
|
||||
Future<void> emotesFuture({
|
||||
required String channelId,
|
||||
required Map<String, String> headers,
|
||||
required Function onError,
|
||||
}) =>
|
||||
// Parallel futures look ugly, but are worth the performance improvement.
|
||||
// Each future has it's own catchError so that the entire future is not ended on an error.
|
||||
// This will ensure that other futures complete even if one fails.
|
||||
Future.wait([
|
||||
ffzApi.getEmotesGlobal().catchError(onError),
|
||||
bttvApi.getEmotesGlobal().catchError(onError),
|
||||
bttvApi.getEmotesChannel(id: channelId).catchError(onError),
|
||||
twitchApi.getEmotesGlobal(headers: headers).catchError(onError),
|
||||
twitchApi
|
||||
.getEmotesChannel(id: channelId, headers: headers)
|
||||
.then((emotes) {
|
||||
_userEmoteSectionToEmotes.update(
|
||||
'Channel Emotes',
|
||||
(existingEmoteSet) => [...existingEmoteSet, ...emotes],
|
||||
ifAbsent: () => emotes.toList(),
|
||||
);
|
||||
return emotes;
|
||||
}).catchError(onEmoteError),
|
||||
],
|
||||
if (show7TVEmotes) ...[
|
||||
sevenTVApi.getEmotesGlobal().catchError(onEmoteError),
|
||||
sevenTVApi.getEmotesChannel(id: channelId).then((data) {
|
||||
final (setId, emotes) = data;
|
||||
sevenTvEmoteSetId = setId;
|
||||
return emotes;
|
||||
}).catchError(onEmoteError),
|
||||
],
|
||||
if (showBTTVEmotes) ...[
|
||||
bttvApi.getEmotesGlobal().catchError(onEmoteError),
|
||||
bttvApi.getEmotesChannel(id: channelId).catchError(onEmoteError),
|
||||
],
|
||||
if (showFFZEmotes) ...[
|
||||
ffzApi.getEmotesGlobal().catchError(onEmoteError),
|
||||
ffzApi.getRoomInfo(id: channelId).then((ffzRoom) {
|
||||
final (roomInfo, emotes) = ffzRoom;
|
||||
|
||||
return emotes;
|
||||
}).catchError(onError),
|
||||
sevenTVApi.getEmotesGlobal().catchError(onError),
|
||||
sevenTVApi.getEmotesChannel(id: channelId).then((data) {
|
||||
final (setId, emotes) = data;
|
||||
sevenTvEmoteSetId = setId;
|
||||
return emotes;
|
||||
}).catchError(onError),
|
||||
ffzApi.getRoomInfo(id: channelId).then((ffzRoom) {
|
||||
final (roomInfo, emotes) = ffzRoom;
|
||||
|
||||
ffzRoomInfo = roomInfo;
|
||||
return emotes;
|
||||
}).catchError(onError),
|
||||
]).then((assets) => assets.expand((list) => list)).then(
|
||||
(emotes) => _emoteToObject = {
|
||||
for (final emote in emotes) emote.name: emote,
|
||||
}.asObservable(),
|
||||
);
|
||||
|
||||
@action
|
||||
Future<void> badgesFuture({
|
||||
required String channelId,
|
||||
required Map<String, String> headers,
|
||||
required Function onError,
|
||||
}) =>
|
||||
Future.wait([
|
||||
// Get global badges first, then channel badges to avoid badge conflicts.
|
||||
// We want the channel badges to override the global badges.
|
||||
twitchApi
|
||||
.getBadgesGlobal(headers: headers)
|
||||
.then((badges) => twitchBadgesToObject.addAll(badges))
|
||||
.then(
|
||||
(_) => twitchApi
|
||||
.getBadgesChannel(id: channelId, headers: headers)
|
||||
.then((badges) => twitchBadgesToObject.addAll(badges))
|
||||
.catchError(onError),
|
||||
ffzRoomInfo = roomInfo;
|
||||
return emotes;
|
||||
}).catchError(onEmoteError),
|
||||
],
|
||||
]).then((assets) => assets.expand((list) => list)).then(
|
||||
(emotes) => _emoteToObject = {
|
||||
for (final emote in emotes) emote.name: emote,
|
||||
}.asObservable(),
|
||||
),
|
||||
ffzApi
|
||||
.getBadges()
|
||||
.then((badges) => _userToFFZBadges = badges)
|
||||
.catchError(onError),
|
||||
bttvApi
|
||||
.getBadges()
|
||||
.then((badges) => _userToBTTVBadges = badges)
|
||||
.catchError(onError),
|
||||
//Emotes
|
||||
Future.wait([
|
||||
// Get global badges first, then channel badges to avoid badge conflicts.
|
||||
// We want the channel badges to override the global badges.
|
||||
if (showTwitchBadges)
|
||||
twitchApi
|
||||
.getBadgesGlobal(headers: headers)
|
||||
.then((badges) => twitchBadgesToObject.addAll(badges))
|
||||
.then(
|
||||
(_) => twitchApi
|
||||
.getBadgesChannel(id: channelId, headers: headers)
|
||||
.then((badges) => twitchBadgesToObject.addAll(badges))
|
||||
.catchError(onBadgeError),
|
||||
),
|
||||
if (showFFZBadges)
|
||||
ffzApi
|
||||
.getBadges()
|
||||
.then((badges) => _userToFFZBadges = badges)
|
||||
.catchError(onBadgeError),
|
||||
if (showBTTVBadges)
|
||||
bttvApi
|
||||
.getBadges()
|
||||
.then((badges) => _userToBTTVBadges = badges)
|
||||
.catchError(onBadgeError),
|
||||
]),
|
||||
]);
|
||||
|
||||
@action
|
||||
|
|
|
|||
|
|
@ -203,7 +203,14 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
|
|||
{required String channelId,
|
||||
required Map<String, String> headers,
|
||||
required Function onEmoteError,
|
||||
required Function onBadgeError}) {
|
||||
required Function onBadgeError,
|
||||
bool showTwitchEmotes = true,
|
||||
bool showTwitchBadges = true,
|
||||
bool show7TVEmotes = true,
|
||||
bool showBTTVEmotes = true,
|
||||
bool showBTTVBadges = true,
|
||||
bool showFFZEmotes = true,
|
||||
bool showFFZBadges = true}) {
|
||||
final _$actionInfo = _$ChatAssetsStoreBaseActionController.startAction(
|
||||
name: 'ChatAssetsStoreBase.assetsFuture');
|
||||
try {
|
||||
|
|
@ -211,37 +218,14 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
|
|||
channelId: channelId,
|
||||
headers: headers,
|
||||
onEmoteError: onEmoteError,
|
||||
onBadgeError: onBadgeError);
|
||||
} finally {
|
||||
_$ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> emotesFuture(
|
||||
{required String channelId,
|
||||
required Map<String, String> headers,
|
||||
required Function onError}) {
|
||||
final _$actionInfo = _$ChatAssetsStoreBaseActionController.startAction(
|
||||
name: 'ChatAssetsStoreBase.emotesFuture');
|
||||
try {
|
||||
return super.emotesFuture(
|
||||
channelId: channelId, headers: headers, onError: onError);
|
||||
} finally {
|
||||
_$ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> badgesFuture(
|
||||
{required String channelId,
|
||||
required Map<String, String> headers,
|
||||
required Function onError}) {
|
||||
final _$actionInfo = _$ChatAssetsStoreBaseActionController.startAction(
|
||||
name: 'ChatAssetsStoreBase.badgesFuture');
|
||||
try {
|
||||
return super.badgesFuture(
|
||||
channelId: channelId, headers: headers, onError: onError);
|
||||
onBadgeError: onBadgeError,
|
||||
showTwitchEmotes: showTwitchEmotes,
|
||||
showTwitchBadges: showTwitchBadges,
|
||||
show7TVEmotes: show7TVEmotes,
|
||||
showBTTVEmotes: showBTTVEmotes,
|
||||
showBTTVBadges: showBTTVBadges,
|
||||
showFFZEmotes: showFFZEmotes,
|
||||
showFFZBadges: showFFZBadges);
|
||||
} finally {
|
||||
_$ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/models/events.dart';
|
||||
import 'package:frosty/models/irc.dart';
|
||||
|
|
@ -26,6 +27,8 @@ abstract class ChatStoreBase with Store {
|
|||
/// The maximum ammount of messages to render when autoscroll is enabled.
|
||||
static const _renderMessageLimit = 100;
|
||||
|
||||
final TwitchApi twitchApi;
|
||||
|
||||
/// The amount of messages to free (remove) when the [_messageLimit] is reached.
|
||||
final _messagesToRemove = (_messageLimit * 0.2).toInt();
|
||||
|
||||
|
|
@ -47,6 +50,8 @@ abstract class ChatStoreBase with Store {
|
|||
/// The channel's display name to show on widgets.
|
||||
final String displayName;
|
||||
|
||||
var _shouldDisconnect = false;
|
||||
|
||||
/// The Twitch IRC WebSocket channel.
|
||||
WebSocketChannel? _channel;
|
||||
|
||||
|
|
@ -55,8 +60,6 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
WebSocketChannel? _sevenTVChannel;
|
||||
|
||||
StreamSubscription? _sevenTVChannelListener;
|
||||
|
||||
static const _maxRetries = 5;
|
||||
|
||||
// The retry counter for exponential backoff.
|
||||
|
|
@ -84,7 +87,7 @@ abstract class ChatStoreBase with Store {
|
|||
final reactions = <ReactionDisposer>[];
|
||||
|
||||
/// The periodic timer used for batching chat message re-renders.
|
||||
late final Timer _messageBufferTimer;
|
||||
Timer? _messageBufferTimer;
|
||||
|
||||
/// The list of chat messages to add once autoscroll is resumed.
|
||||
/// This is used as an optimization to prevent the list from being updated/shifted while the user is scrolling.
|
||||
|
|
@ -150,6 +153,7 @@ abstract class ChatStoreBase with Store {
|
|||
IRCMessage? replyingToMessage;
|
||||
|
||||
ChatStoreBase({
|
||||
required this.twitchApi,
|
||||
required this.auth,
|
||||
required this.chatDetailsStore,
|
||||
required this.assetsStore,
|
||||
|
|
@ -166,23 +170,44 @@ abstract class ChatStoreBase with Store {
|
|||
reactions.add(
|
||||
reaction(
|
||||
(_) => auth.isLoggedIn,
|
||||
(_) => _channel?.sink.close(1001),
|
||||
(_) => _channel?.sink.close(1000),
|
||||
),
|
||||
);
|
||||
|
||||
// Create a timer that will add messages from the buffer every 200 milliseconds.
|
||||
_messageBufferTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 200),
|
||||
(timer) => addMessages(),
|
||||
reactions.add(
|
||||
reaction(
|
||||
(_) => [
|
||||
settings.showTwitchEmotes,
|
||||
settings.showTwitchBadges,
|
||||
settings.show7TVEmotes,
|
||||
settings.showBTTVEmotes,
|
||||
settings.showBTTVBadges,
|
||||
settings.showFFZEmotes,
|
||||
settings.showFFZBadges,
|
||||
],
|
||||
(_) {
|
||||
if (!settings.show7TVEmotes) {
|
||||
_sevenTVChannel?.sink.close(1000);
|
||||
}
|
||||
|
||||
getAssets().then((_) {
|
||||
if (settings.show7TVEmotes) {
|
||||
final emoteSetId = assetsStore.sevenTvEmoteSetId;
|
||||
if (emoteSetId != null) {
|
||||
listenToSevenTVEmoteSet(emoteSetId: emoteSetId);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
assetsStore.init();
|
||||
|
||||
messageBuffer
|
||||
.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
||||
_messages.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
||||
|
||||
if (settings.chatDelay > 0) {
|
||||
messageBuffer.add(
|
||||
if (settings.showVideo && settings.chatDelay > 0) {
|
||||
_messages.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
'Waiting ${settings.chatDelay.toInt()} ${settings.chatDelay == 1.0 ? 'second' : 'seconds'} due to message delay setting...',
|
||||
|
|
@ -190,7 +215,35 @@ abstract class ChatStoreBase with Store {
|
|||
);
|
||||
}
|
||||
|
||||
connectToChat();
|
||||
reactions.add(
|
||||
reaction(
|
||||
(_) => settings.showVideo,
|
||||
(showVideo) {
|
||||
if (settings.chatDelay > 0) {
|
||||
if (showVideo) {
|
||||
_messages.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
'Waiting ${settings.chatDelay.toInt()} ${settings.chatDelay == 1.0 ? 'second' : 'seconds'} due to message delay setting...',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_messages.add(
|
||||
IRCMessage.createNotice(
|
||||
message: 'Removing message delay...',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (settings.showRecentMessages) {
|
||||
getRecentMessage().then((_) => connectToChat());
|
||||
} else {
|
||||
connectToChat();
|
||||
}
|
||||
|
||||
// Tell the scrollController to determine when auto-scroll should be enabled or disabled.
|
||||
scrollController.addListener(() {
|
||||
|
|
@ -329,13 +382,23 @@ abstract class ChatStoreBase with Store {
|
|||
messageBuffer.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
"Connected to ${getReadableName(displayName, channelName)}'s chat!",
|
||||
"Welcome to ${getReadableName(displayName, channelName)}'s chat!",
|
||||
),
|
||||
);
|
||||
|
||||
// Activate the message buffer.
|
||||
// Create a timer that will add messages from the buffer every 200 milliseconds.
|
||||
_messageBufferTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 200),
|
||||
(timer) => addMessages(),
|
||||
);
|
||||
|
||||
getAssets().then((_) {
|
||||
if (assetsStore.sevenTvEmoteSetId != null) {
|
||||
listenToSevenTVEmoteSet(emoteSetId: assetsStore.sevenTvEmoteSetId!);
|
||||
if (!settings.show7TVEmotes) return;
|
||||
|
||||
final emoteSetId = assetsStore.sevenTvEmoteSetId;
|
||||
if (emoteSetId != null && !_shouldDisconnect) {
|
||||
listenToSevenTVEmoteSet(emoteSetId: emoteSetId);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -359,6 +422,13 @@ abstract class ChatStoreBase with Store {
|
|||
debugPrint(error.toString());
|
||||
return <Badge>[];
|
||||
},
|
||||
showTwitchEmotes: settings.showTwitchEmotes,
|
||||
showTwitchBadges: settings.showTwitchBadges,
|
||||
show7TVEmotes: settings.show7TVEmotes,
|
||||
showBTTVEmotes: settings.showBTTVEmotes,
|
||||
showBTTVBadges: settings.showBTTVBadges,
|
||||
showFFZEmotes: settings.showFFZEmotes,
|
||||
showFFZBadges: settings.showFFZBadges,
|
||||
);
|
||||
|
||||
/// Re-enables [_autoScroll] and jumps to the latest message.
|
||||
|
|
@ -385,56 +455,65 @@ abstract class ChatStoreBase with Store {
|
|||
),
|
||||
);
|
||||
|
||||
_sevenTVChannel?.sink.close(1001);
|
||||
_sevenTVChannel?.sink.close(1000);
|
||||
_sevenTVChannel =
|
||||
WebSocketChannel.connect(Uri.parse('wss://events.7tv.io/v3'));
|
||||
|
||||
_sevenTVChannelListener = _sevenTVChannel?.stream.listen(
|
||||
(data) => Future.delayed(
|
||||
Duration(seconds: settings.chatDelay.toInt()),
|
||||
() {
|
||||
debugPrint(data);
|
||||
final decoded = jsonDecode(data);
|
||||
void listener(dynamic data) {
|
||||
// debugPrint(data);
|
||||
final decoded = jsonDecode(data);
|
||||
|
||||
final event = SevenTVEvent.fromJson(decoded);
|
||||
final event = SevenTVEvent.fromJson(decoded);
|
||||
|
||||
final body = event.d.body;
|
||||
if (event.d.type != 'emote_set.update' || body == null) return;
|
||||
final body = event.d.body;
|
||||
if (event.d.type != 'emote_set.update' || body == null) return;
|
||||
|
||||
if (body.pushed != null) {
|
||||
final pushedEmote = body.pushed?.first.value;
|
||||
if (body.pushed != null) {
|
||||
final pushedEmote = body.pushed?.first.value;
|
||||
|
||||
if (pushedEmote == null) return;
|
||||
if (pushedEmote == null) return;
|
||||
|
||||
final emote = Emote.from7TV(pushedEmote, EmoteType.sevenTVChannel);
|
||||
final emote = Emote.from7TV(pushedEmote, EmoteType.sevenTVChannel);
|
||||
|
||||
assetsStore.emoteToObject[emote.name] = emote;
|
||||
assetsStore.emoteToObject[emote.name] = emote;
|
||||
|
||||
messageBuffer.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
'${getReadableName(body.actor.displayName, body.actor.username)} added 7TV emote "${emote.name}" to chat',
|
||||
),
|
||||
);
|
||||
} else if (body.pulled != null) {
|
||||
final pulledEmote = body.pulled?.first.oldValue;
|
||||
messageBuffer.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
'${getReadableName(body.actor.displayName, body.actor.username)} added 7TV emote "${emote.name}" to chat',
|
||||
),
|
||||
);
|
||||
} else if (body.pulled != null) {
|
||||
final pulledEmote = body.pulled?.first.oldValue;
|
||||
|
||||
if (pulledEmote == null) return;
|
||||
if (pulledEmote == null) return;
|
||||
|
||||
assetsStore.emoteToObject.removeWhere(
|
||||
(name, _) => name == pulledEmote.name,
|
||||
);
|
||||
assetsStore.emoteToObject.removeWhere(
|
||||
(name, _) => name == pulledEmote.name,
|
||||
);
|
||||
|
||||
messageBuffer.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
'${getReadableName(body.actor.displayName, body.actor.username)} removed 7TV emote "${pulledEmote.name}" from chat',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
messageBuffer.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
'${getReadableName(body.actor.displayName, body.actor.username)} removed 7TV emote "${pulledEmote.name}" from chat',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_sevenTVChannel?.stream.listen(
|
||||
(data) {
|
||||
if (!settings.showVideo || settings.chatDelay == 0) {
|
||||
listener(data);
|
||||
} else {
|
||||
Future.delayed(
|
||||
Duration(seconds: settings.chatDelay.toInt()),
|
||||
() => listener(data),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => debugPrint('7TV events error: ${error.toString()}'),
|
||||
onDone: () => debugPrint('7TV events done'),
|
||||
);
|
||||
|
||||
_sevenTVChannel?.sink.add(jsonEncode(subscribePayload));
|
||||
|
|
@ -442,19 +521,28 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
@action
|
||||
void connectToChat() {
|
||||
_channel?.sink.close(1001);
|
||||
_channel?.sink.close(1000);
|
||||
_channel =
|
||||
WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443'));
|
||||
|
||||
// Listen for new messages and forward them to the handler.
|
||||
_channelListener = _channel?.stream.listen(
|
||||
(data) => Future.delayed(
|
||||
Duration(seconds: settings.chatDelay.toInt()),
|
||||
() => _handleIRCData(data.toString()),
|
||||
),
|
||||
(data) {
|
||||
if (!settings.showVideo || settings.chatDelay == 0) {
|
||||
_handleIRCData(data.toString());
|
||||
} else {
|
||||
Future.delayed(
|
||||
Duration(seconds: settings.chatDelay.toInt()),
|
||||
() => _handleIRCData(data.toString()),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => debugPrint('Chat error: ${error.toString()}'),
|
||||
onDone: () async {
|
||||
if (_channel == null) return;
|
||||
if (_shouldDisconnect) {
|
||||
_sevenTVChannel?.sink.close(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_retries >= _maxRetries) {
|
||||
messageBuffer.add(
|
||||
|
|
@ -655,19 +743,25 @@ abstract class ChatStoreBase with Store {
|
|||
timeRemaining = const Duration();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> getRecentMessage() async {
|
||||
final recentMessages =
|
||||
await twitchApi.getRecentMessages(userLogin: channelName);
|
||||
|
||||
for (final message in recentMessages) {
|
||||
_handleIRCData(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes and disposes all the channels and controllers used by the store.
|
||||
void dispose() {
|
||||
_messageBufferTimer.cancel();
|
||||
_shouldDisconnect = true;
|
||||
|
||||
_messageBufferTimer?.cancel();
|
||||
_notificationTimer?.cancel();
|
||||
sleepTimer?.cancel();
|
||||
|
||||
_sevenTVChannel?.sink.close(1001);
|
||||
_sevenTVChannel = null;
|
||||
_sevenTVChannelListener?.cancel();
|
||||
|
||||
_channel?.sink.close(1001);
|
||||
_channel = null;
|
||||
_channelListener?.cancel();
|
||||
_channel?.sink.close(1000);
|
||||
|
||||
for (final reactionDisposer in reactions) {
|
||||
reactionDisposer();
|
||||
|
|
|
|||
|
|
@ -219,6 +219,14 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
return _$getAssetsAsyncAction.run(() => super.getAssets());
|
||||
}
|
||||
|
||||
late final _$getRecentMessageAsyncAction =
|
||||
AsyncAction('ChatStoreBase.getRecentMessage', context: context);
|
||||
|
||||
@override
|
||||
Future<void> getRecentMessage() {
|
||||
return _$getRecentMessageAsyncAction.run(() => super.getRecentMessage());
|
||||
}
|
||||
|
||||
late final _$ChatStoreBaseActionController =
|
||||
ActionController(name: 'ChatStoreBase', context: context);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,25 +14,32 @@ class ChatBottomBar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final emoteMenuButton = Tooltip(
|
||||
message: 'Emote menu',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
color: chatStore.assetsStore.showEmoteMenu
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: null,
|
||||
icon: Icon(
|
||||
chatStore.assetsStore.showEmoteMenu
|
||||
? Icons.emoji_emotions_rounded
|
||||
: Icons.emoji_emotions_outlined,
|
||||
),
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatStore.assetsStore.showEmoteMenu =
|
||||
!chatStore.assetsStore.showEmoteMenu;
|
||||
},
|
||||
),
|
||||
);
|
||||
final isEmotesEnabled = chatStore.settings.showTwitchEmotes ||
|
||||
chatStore.settings.show7TVEmotes ||
|
||||
chatStore.settings.showBTTVEmotes ||
|
||||
chatStore.settings.showFFZEmotes;
|
||||
|
||||
final emoteMenuButton = isEmotesEnabled
|
||||
? Tooltip(
|
||||
message: 'Emote menu',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
color: chatStore.assetsStore.showEmoteMenu
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: null,
|
||||
icon: Icon(
|
||||
chatStore.assetsStore.showEmoteMenu
|
||||
? Icons.emoji_emotions_rounded
|
||||
: Icons.emoji_emotions_outlined,
|
||||
),
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatStore.assetsStore.showEmoteMenu =
|
||||
!chatStore.assetsStore.showEmoteMenu;
|
||||
},
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
|
|
@ -172,79 +179,73 @@ class ChatBottomBar extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 0, 12),
|
||||
child: Column(
|
||||
padding: const EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (!chatStore.expandChat &&
|
||||
chatStore.settings.chatWidth < 0.3 &&
|
||||
chatStore.settings.showVideo &&
|
||||
MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape)
|
||||
IconButton(
|
||||
tooltip: 'Enter a message',
|
||||
onPressed: () {
|
||||
chatStore.expandChat = true;
|
||||
chatStore.textFieldFocusNode.requestFocus();
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: TextField(
|
||||
textInputAction: TextInputAction.send,
|
||||
focusNode: chatStore.textFieldFocusNode,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
enabled: chatStore.auth.isLoggedIn ? true : false,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon:
|
||||
chatStore.settings.emoteMenuButtonOnLeft
|
||||
? emoteMenuButton
|
||||
: null,
|
||||
suffixIcon:
|
||||
chatStore.settings.emoteMenuButtonOnLeft
|
||||
? null
|
||||
: emoteMenuButton,
|
||||
hintMaxLines: 1,
|
||||
hintText: chatStore.auth.isLoggedIn
|
||||
? 'Send a ${chatStore.replyingToMessage != null ? 'reply' : 'message'} ${chatStore.settings.chatDelay == 0 ? '' : '(${chatStore.settings.chatDelay.toInt()}s delay)'}'
|
||||
: 'Log in to chat',
|
||||
),
|
||||
controller: chatStore.textController,
|
||||
onSubmitted: chatStore.sendMessage,
|
||||
),
|
||||
),
|
||||
if (chatStore.showSendButton &&
|
||||
(chatStore.settings.chatWidth >= 0.3 ||
|
||||
chatStore.expandChat ||
|
||||
MediaQuery.of(context).orientation ==
|
||||
Orientation.portrait))
|
||||
IconButton(
|
||||
tooltip: 'Send',
|
||||
icon: const Icon(Icons.send_rounded),
|
||||
onPressed: chatStore.auth.isLoggedIn
|
||||
? () => chatStore
|
||||
.sendMessage(chatStore.textController.text)
|
||||
if (!chatStore.expandChat &&
|
||||
chatStore.settings.chatWidth < 0.3 &&
|
||||
chatStore.settings.showVideo &&
|
||||
MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape)
|
||||
IconButton(
|
||||
tooltip: 'Enter a message',
|
||||
onPressed: () {
|
||||
chatStore.expandChat = true;
|
||||
chatStore.textFieldFocusNode.requestFocus();
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: TextField(
|
||||
textInputAction: TextInputAction.send,
|
||||
focusNode: chatStore.textFieldFocusNode,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
enabled: chatStore.auth.isLoggedIn ? true : false,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: chatStore.settings.emoteMenuButtonOnLeft
|
||||
? emoteMenuButton
|
||||
: null,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: Icon(Icons.adaptive.more_rounded),
|
||||
tooltip: 'More',
|
||||
onPressed: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (_) => ChatDetails(
|
||||
chatDetailsStore: chatStore.chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: chatStore.channelName,
|
||||
),
|
||||
),
|
||||
suffixIcon: chatStore.settings.emoteMenuButtonOnLeft
|
||||
? null
|
||||
: emoteMenuButton,
|
||||
hintMaxLines: 1,
|
||||
hintText: chatStore.auth.isLoggedIn
|
||||
? 'Send a ${chatStore.replyingToMessage != null ? 'reply' : 'message'} ${chatStore.settings.chatDelay == 0 || !chatStore.settings.showVideo ? '' : '(${chatStore.settings.chatDelay.toInt()}s delay)'}'
|
||||
: 'Log in to chat',
|
||||
),
|
||||
],
|
||||
),
|
||||
controller: chatStore.textController,
|
||||
onSubmitted: chatStore.sendMessage,
|
||||
),
|
||||
),
|
||||
if (chatStore.showSendButton &&
|
||||
(chatStore.settings.chatWidth >= 0.3 ||
|
||||
chatStore.expandChat ||
|
||||
MediaQuery.of(context).orientation ==
|
||||
Orientation.portrait))
|
||||
IconButton(
|
||||
tooltip: 'Send',
|
||||
icon: const Icon(Icons.send_rounded),
|
||||
onPressed: chatStore.auth.isLoggedIn
|
||||
? () => chatStore
|
||||
.sendMessage(chatStore.textController.text)
|
||||
: null,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: Icon(Icons.adaptive.more_rounded),
|
||||
tooltip: 'More',
|
||||
onPressed: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (_) => ChatDetails(
|
||||
chatDetailsStore: chatStore.chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: chatStore.channelName,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -55,16 +55,18 @@ class ChatMessage extends StatelessWidget {
|
|||
userLogin: nickname,
|
||||
)
|
||||
.then((user) {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: chatStore,
|
||||
username: user.login,
|
||||
userId: user.id,
|
||||
displayName: user.displayName,
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: chatStore,
|
||||
username: user.login,
|
||||
userId: user.id,
|
||||
displayName: user.displayName,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -178,10 +180,9 @@ class ChatMessage extends StatelessWidget {
|
|||
const mentionBorderRadius = BorderRadius.all(Radius.circular(4));
|
||||
if (replyUser != null && replyBody != null) {
|
||||
messageHeaderIcon = Icon(
|
||||
Icons.reply_rounded,
|
||||
Icons.chat_rounded,
|
||||
size: messageHeaderIconSize,
|
||||
color: messageHeaderTextColor,
|
||||
textDirection: TextDirection.rtl,
|
||||
);
|
||||
messageHeader = GestureDetector(
|
||||
onTap: isModal
|
||||
|
|
@ -476,7 +477,7 @@ class ChatMessage extends StatelessWidget {
|
|||
child: dividedMessage,
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
final finalMessage = InkWell(
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (chatStore.assetsStore.showEmoteMenu) {
|
||||
|
|
@ -486,6 +487,17 @@ class ChatMessage extends StatelessWidget {
|
|||
onLongPress: () => onLongPressMessage(context),
|
||||
child: coloredMessage,
|
||||
);
|
||||
|
||||
final isHistorical = ircMessage.tags['historical'] == '1';
|
||||
|
||||
if (chatStore.settings.darkenRecentMessages && isHistorical) {
|
||||
return Opacity(
|
||||
opacity: 0.5,
|
||||
child: finalMessage,
|
||||
);
|
||||
}
|
||||
|
||||
return finalMessage;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
|||
import 'package:frosty/screens/channel/video/video_bar.dart';
|
||||
import 'package:frosty/screens/channel/video/video_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:frosty/widgets/uptime.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
|
@ -104,10 +105,11 @@ class VideoOverlay extends StatelessWidget {
|
|||
children: [
|
||||
Observer(
|
||||
builder: (context) => Text(
|
||||
videoStore.latency ?? '',
|
||||
videoStore.latency ?? 'N/A',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -115,7 +117,8 @@ class VideoOverlay extends StatelessWidget {
|
|||
width: 8,
|
||||
),
|
||||
const Icon(
|
||||
Icons.speed,
|
||||
Icons.speed_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -177,37 +180,39 @@ class VideoOverlay extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
|
||||
final streamInfo = videoStore.streamInfo;
|
||||
if (streamInfo == null) {
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
backButton,
|
||||
const Spacer(),
|
||||
if (videoStore.settingsStore.fullScreen &&
|
||||
orientation == Orientation.landscape)
|
||||
chatOverlayButton,
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
refreshButton,
|
||||
if (!videoStore.isIPad) rotateButton,
|
||||
if (orientation == Orientation.landscape) fullScreenButton,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final streamInfo = videoStore.streamInfo;
|
||||
if (streamInfo == null) {
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
backButton,
|
||||
const Spacer(),
|
||||
if (videoStore.settingsStore.fullScreen &&
|
||||
orientation == Orientation.landscape)
|
||||
chatOverlayButton,
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
refreshButton,
|
||||
// On iPad, hide the rotate button on the overlay
|
||||
// Flutter doesn't allow programmatic rotation on iPad unless multitasking is disabled.
|
||||
if (!isIPad()) rotateButton,
|
||||
if (orientation == Orientation.landscape) fullScreenButton,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
|
|
@ -227,7 +232,7 @@ class VideoOverlay extends StatelessWidget {
|
|||
if (videoStore.settingsStore.fullScreen &&
|
||||
orientation == Orientation.landscape)
|
||||
chatOverlayButton,
|
||||
if (!Platform.isIOS) videoSettingsButton,
|
||||
if (!Platform.isIOS || isIPad()) videoSettingsButton,
|
||||
],
|
||||
),
|
||||
Center(
|
||||
|
|
@ -249,11 +254,10 @@ class VideoOverlay extends StatelessWidget {
|
|||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
|
|
@ -314,6 +318,9 @@ class VideoOverlay extends StatelessWidget {
|
|||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFeatures: [
|
||||
FontFeature.tabularFigures(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -339,7 +346,9 @@ class VideoOverlay extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
refreshButton,
|
||||
if (!videoStore.isIPad) rotateButton,
|
||||
// On iPad, hide the rotate button on the overlay
|
||||
// Flutter doesn't allow programmatic rotation on iPad unless multitasking is disabled.
|
||||
if (!isIPad()) rotateButton,
|
||||
if (orientation == Orientation.landscape) fullScreenButton,
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
|
|
@ -45,7 +44,17 @@ abstract class VideoStoreBase with Store {
|
|||
..addJavaScriptChannel(
|
||||
'Latency',
|
||||
onMessageReceived: (message) {
|
||||
_latency = message.message;
|
||||
final receivedLatency = message.message;
|
||||
_latency = receivedLatency;
|
||||
|
||||
if (!settingsStore.autoSyncChatDelay) return;
|
||||
|
||||
final trimmedLatency = receivedLatency.split(' ')[0];
|
||||
final latencyAsDouble = double.tryParse(trimmedLatency);
|
||||
|
||||
if (latencyAsDouble != null) {
|
||||
settingsStore.chatDelay = latencyAsDouble;
|
||||
}
|
||||
},
|
||||
)
|
||||
..addJavaScriptChannel(
|
||||
|
|
@ -68,14 +77,7 @@ abstract class VideoStoreBase with Store {
|
|||
onMessageReceived: (message) async {
|
||||
if (settingsStore.defaultToHighestQuality &&
|
||||
_firstTimeSettingQuality) {
|
||||
await updateStreamQualities();
|
||||
|
||||
await setStreamQuality(
|
||||
_availableStreamQualities.firstWhere(
|
||||
(quality) => quality != 'Auto',
|
||||
orElse: () => 'Auto',
|
||||
),
|
||||
);
|
||||
await _setStreamQualityIndex(1);
|
||||
|
||||
_firstTimeSettingQuality = false;
|
||||
} else {
|
||||
|
|
@ -115,10 +117,6 @@ abstract class VideoStoreBase with Store {
|
|||
@readonly
|
||||
var _overlayVisible = true;
|
||||
|
||||
/// If the current device is iPad.
|
||||
@readonly
|
||||
var _isIPad = false;
|
||||
|
||||
/// The current stream info, used for displaying relevant info on the overlay.
|
||||
@readonly
|
||||
StreamTwitch? _streamInfo;
|
||||
|
|
@ -126,9 +124,13 @@ abstract class VideoStoreBase with Store {
|
|||
@readonly
|
||||
List<String> _availableStreamQualities = [];
|
||||
|
||||
// The current stream quality string
|
||||
// The current stream quality index
|
||||
@readonly
|
||||
String _streamQuality = 'Auto';
|
||||
int _streamQualityIndex = 0;
|
||||
|
||||
// The current stream quality string
|
||||
String get streamQuality =>
|
||||
_availableStreamQualities.elementAtOrNull(_streamQualityIndex) ?? 'Auto';
|
||||
|
||||
@readonly
|
||||
String? _latency;
|
||||
|
|
@ -190,12 +192,27 @@ abstract class VideoStoreBase with Store {
|
|||
try {
|
||||
await videoWebViewController.runJavaScript('''
|
||||
{
|
||||
document.querySelector('[data-a-target="player-settings-button"]').click();
|
||||
document.querySelector('[data-a-target="player-settings-menu-item-quality"]').click();
|
||||
const qualities = [...document.querySelectorAll('[data-a-target="player-settings-submenu-quality-option"] label div')].map((el) => el.textContent);
|
||||
document.querySelector('.tw-drop-down-menu-item-figure').click();
|
||||
document.querySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button').click();
|
||||
StreamQualities.postMessage(JSON.stringify(qualities));
|
||||
const asyncQuerySelector = (selector) => new Promise((resolve) => {
|
||||
if (document.querySelector(selector)) {
|
||||
return resolve(document.querySelector(selector));
|
||||
}
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (document.querySelector(selector)) {
|
||||
observer.disconnect();
|
||||
resolve(document.querySelector(selector));
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
(async () => {
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-button"]')).click();
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-menu-item-quality"]')).click();
|
||||
await asyncQuerySelector('[data-a-target="player-settings-submenu-quality-option"] label div');
|
||||
const qualities = [...document.querySelectorAll('[data-a-target="player-settings-submenu-quality-option"] label div')].map((el) => el.textContent);
|
||||
StreamQualities.postMessage(JSON.stringify(qualities));
|
||||
(await asyncQuerySelector('.tw-drop-down-menu-item-figure')).click();
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button')).click();
|
||||
})();
|
||||
}
|
||||
''');
|
||||
} catch (e) {
|
||||
|
|
@ -207,60 +224,106 @@ abstract class VideoStoreBase with Store {
|
|||
Future<void> setStreamQuality(String newStreamQuality) async {
|
||||
final indexOfStreamQuality =
|
||||
_availableStreamQualities.indexOf(newStreamQuality);
|
||||
await videoWebViewController.runJavaScript('''
|
||||
await _setStreamQualityIndex(indexOfStreamQuality);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> _setStreamQualityIndex(int newStreamQualityIndex) async {
|
||||
try {
|
||||
await videoWebViewController.runJavaScript('''
|
||||
{
|
||||
document.querySelector('[data-a-target="player-settings-button"]').click();
|
||||
document.querySelector('[data-a-target="player-settings-menu-item-quality"]').click();
|
||||
[...document.querySelectorAll('[data-a-target="player-settings-submenu-quality-option"] input')][$indexOfStreamQuality].click();
|
||||
document.querySelector('.tw-drop-down-menu-item-figure').click();
|
||||
document.querySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button').click();
|
||||
const asyncQuerySelector = (selector) => new Promise((resolve) => {
|
||||
if (document.querySelector(selector)) {
|
||||
return resolve(document.querySelector(selector));
|
||||
}
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (document.querySelector(selector)) {
|
||||
observer.disconnect();
|
||||
resolve(document.querySelector(selector));
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
(async () => {
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-button"]')).click();
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-menu-item-quality"]')).click();
|
||||
await asyncQuerySelector('[data-a-target="player-settings-submenu-quality-option"] input');
|
||||
[...document.querySelectorAll('[data-a-target="player-settings-submenu-quality-option"] input')][$newStreamQualityIndex].click();
|
||||
(await asyncQuerySelector('.tw-drop-down-menu-item-figure')).click();
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button')).click();
|
||||
})();
|
||||
}
|
||||
''');
|
||||
_streamQuality = newStreamQuality;
|
||||
_streamQualityIndex = newStreamQualityIndex;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _hideDefaultOverlay() {
|
||||
videoWebViewController.runJavaScript('''
|
||||
{
|
||||
const hideElements = (...el) => {
|
||||
el.forEach((el) => {
|
||||
el?.style.setProperty("display", "none", "important");
|
||||
})
|
||||
Future<void> _hideDefaultOverlay() async {
|
||||
try {
|
||||
await videoWebViewController.runJavaScript('''
|
||||
{
|
||||
const hideElements = (...el) => {
|
||||
el.forEach((el) => {
|
||||
el?.style.setProperty("display", "none", "important");
|
||||
})
|
||||
}
|
||||
const hide = () => {
|
||||
const topBar = document.querySelector(".top-bar");
|
||||
const playerControls = document.querySelector(".player-controls");
|
||||
const channelDisclosures = document.querySelector("#channel-player-disclosures");
|
||||
hideElements(topBar, playerControls, channelDisclosures);
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
const videoOverlay = document.querySelector('.video-player__overlay');
|
||||
if(!videoOverlay) return;
|
||||
hide();
|
||||
const videoOverlayObserver = new MutationObserver(hide);
|
||||
videoOverlayObserver.observe(videoOverlay, { childList: true, subtree: true });
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
const hide = () => {
|
||||
const topBar = document.querySelector(".top-bar");
|
||||
const playerControls = document.querySelector(".player-controls");
|
||||
const channelDisclosures = document.querySelector("#channel-player-disclosures");
|
||||
hideElements(topBar, playerControls, channelDisclosures);
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
const videoOverlay = document.querySelector('.video-player__overlay');
|
||||
if(!videoOverlay) return;
|
||||
hide();
|
||||
const videoOverlayObserver = new MutationObserver(hide);
|
||||
videoOverlayObserver.observe(videoOverlay, { childList: true, subtree: true });
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
''');
|
||||
''');
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _listenOnLatencyChanges() {
|
||||
videoWebViewController.runJavaScript('''
|
||||
{
|
||||
document.querySelector('[data-a-target="player-settings-button"]').click();
|
||||
document.querySelector('[data-a-target="player-settings-menu-item-advanced"]').click();
|
||||
document.querySelector('[data-a-target="player-settings-submenu-advanced-video-stats"] input').click();
|
||||
document.querySelector('.tw-drop-down-menu-item-figure').click();
|
||||
document.querySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button').click();
|
||||
document.querySelector('[data-a-target="player-overlay-video-stats"]').style.display = "none";
|
||||
const observer = new MutationObserver((changes) => {
|
||||
Latency.postMessage(changes[0].target.textContent);
|
||||
})
|
||||
observer.observe(document.querySelector('[aria-label="Latency To Broadcaster"]'), { characterData: true, attributes: false, childList: false, subtree: true });
|
||||
Future<void> _listenOnLatencyChanges() async {
|
||||
try {
|
||||
await videoWebViewController.runJavaScript('''
|
||||
{
|
||||
const asyncQuerySelector = (selector) => new Promise((resolve) => {
|
||||
if (document.querySelector(selector)) {
|
||||
return resolve(document.querySelector(selector));
|
||||
}
|
||||
''');
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (document.querySelector(selector)) {
|
||||
observer.disconnect();
|
||||
resolve(document.querySelector(selector));
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
(async () => {
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-button"]')).click();
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-menu-item-advanced"]')).click();
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-submenu-advanced-video-stats"] input')).click();
|
||||
(await asyncQuerySelector('.tw-drop-down-menu-item-figure')).click();
|
||||
(await asyncQuerySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button')).click();
|
||||
(await asyncQuerySelector('[data-a-target="player-overlay-video-stats"]')).style.display = "none";
|
||||
const observer = new MutationObserver((changes) => {
|
||||
Latency.postMessage(changes[0].target.textContent);
|
||||
})
|
||||
observer.observe(document.querySelector('[aria-label="Latency To Broadcaster"]'), { characterData: true, attributes: false, childList: false, subtree: true });
|
||||
})();
|
||||
}
|
||||
''');
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the video webview.
|
||||
|
|
@ -270,38 +333,32 @@ abstract class VideoStoreBase with Store {
|
|||
// Add event listeners to notify the JavaScript channels when the video plays and pauses.
|
||||
try {
|
||||
videoWebViewController.runJavaScript(
|
||||
'''document.getElementsByTagName("video")[0].addEventListener("pause", () => {
|
||||
'''
|
||||
(function checkVideoElement() {
|
||||
const videoElement = document.getElementsByTagName("video")[0];
|
||||
if (videoElement) {
|
||||
videoElement.addEventListener("pause", () => {
|
||||
VideoPause.postMessage("video paused");
|
||||
document.getElementsByTagName("video")[0].textTracks[0].mode = "hidden";
|
||||
});''',
|
||||
);
|
||||
videoWebViewController.runJavaScript(
|
||||
'''document.getElementsByTagName("video")[0].addEventListener("playing", () => {
|
||||
VideoPlaying.postMessage("video playing")
|
||||
document.getElementsByTagName("video")[0].textTracks[0].mode = "hidden";
|
||||
});''',
|
||||
videoElement.textTracks[0].mode = "hidden";
|
||||
});
|
||||
videoElement.addEventListener("playing", () => {
|
||||
VideoPlaying.postMessage("video playing");
|
||||
videoElement.textTracks[0].mode = "hidden";
|
||||
});
|
||||
} else {
|
||||
setTimeout(checkVideoElement, 100); // Check again after 100ms
|
||||
}
|
||||
})();
|
||||
''',
|
||||
);
|
||||
if (settingsStore.showOverlay) {
|
||||
_hideDefaultOverlay();
|
||||
_listenOnLatencyChanges();
|
||||
await _hideDefaultOverlay();
|
||||
await _listenOnLatencyChanges();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether the device is an iPad or not.
|
||||
// Used to show or hide the rotate button on the overlay.
|
||||
// Flutter doesn't allow programmatic rotation on iPad unless multitasking is disabled.
|
||||
if (Platform.isIOS) {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final info = await deviceInfo.iosInfo;
|
||||
if (info.model.toLowerCase().contains('ipad')) {
|
||||
_isIPad = true;
|
||||
} else {
|
||||
_isIPad = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called whenever the video/overlay is tapped.
|
||||
|
|
|
|||
|
|
@ -45,24 +45,6 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_isIPadAtom =
|
||||
Atom(name: 'VideoStoreBase._isIPad', context: context);
|
||||
|
||||
bool get isIPad {
|
||||
_$_isIPadAtom.reportRead();
|
||||
return super._isIPad;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _isIPad => isIPad;
|
||||
|
||||
@override
|
||||
set _isIPad(bool value) {
|
||||
_$_isIPadAtom.reportWrite(value, super._isIPad, () {
|
||||
super._isIPad = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_streamInfoAtom =
|
||||
Atom(name: 'VideoStoreBase._streamInfo', context: context);
|
||||
|
||||
|
|
@ -100,21 +82,21 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_streamQualityAtom =
|
||||
Atom(name: 'VideoStoreBase._streamQuality', context: context);
|
||||
late final _$_streamQualityIndexAtom =
|
||||
Atom(name: 'VideoStoreBase._streamQualityIndex', context: context);
|
||||
|
||||
String get streamQuality {
|
||||
_$_streamQualityAtom.reportRead();
|
||||
return super._streamQuality;
|
||||
int get streamQualityIndex {
|
||||
_$_streamQualityIndexAtom.reportRead();
|
||||
return super._streamQualityIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
String get _streamQuality => streamQuality;
|
||||
int get _streamQualityIndex => streamQualityIndex;
|
||||
|
||||
@override
|
||||
set _streamQuality(String value) {
|
||||
_$_streamQualityAtom.reportWrite(value, super._streamQuality, () {
|
||||
super._streamQuality = value;
|
||||
set _streamQualityIndex(int value) {
|
||||
_$_streamQualityIndexAtom.reportWrite(value, super._streamQualityIndex, () {
|
||||
super._streamQualityIndex = value;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +136,15 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
.run(() => super.setStreamQuality(newStreamQuality));
|
||||
}
|
||||
|
||||
late final _$_setStreamQualityIndexAsyncAction =
|
||||
AsyncAction('VideoStoreBase._setStreamQualityIndex', context: context);
|
||||
|
||||
@override
|
||||
Future<void> _setStreamQualityIndex(int newStreamQualityIndex) {
|
||||
return _$_setStreamQualityIndexAsyncAction
|
||||
.run(() => super._setStreamQualityIndex(newStreamQualityIndex));
|
||||
}
|
||||
|
||||
late final _$initVideoAsyncAction =
|
||||
AsyncAction('VideoStoreBase.initVideo', context: context);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import 'package:frosty/screens/home/top/top.dart';
|
|||
import 'package:frosty/screens/settings/settings.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/release_notes.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
const Home({super.key});
|
||||
|
|
@ -24,6 +27,29 @@ class _HomeState extends State<Home> {
|
|||
|
||||
late final _homeStore = HomeStore(authStore: _authStore);
|
||||
|
||||
Future<void> checkAndShowReleaseNotes() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final currentVersion = packageInfo.version;
|
||||
final storedVersion = prefs.getString('last_shown_version');
|
||||
|
||||
if (storedVersion == null || storedVersion != currentVersion) {
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const ReleaseNotes()),
|
||||
).then((_) => prefs.setString('last_shown_version', currentVersion));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
checkAndShowReleaseNotes();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SystemChrome.setPreferredOrientations([]);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
try {
|
||||
final channelInfo = await widget.searchStore.searchChannel(search);
|
||||
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
|
|||
|
|
@ -83,6 +83,16 @@ abstract class SearchStoreBase with Store {
|
|||
query: query,
|
||||
headers: authStore.headersTwitch,
|
||||
)
|
||||
.then((categories) {
|
||||
// Move exact matches to the first result
|
||||
final matchingIndex = categories.data
|
||||
.indexWhere((c) => c.name.toLowerCase() == query.toLowerCase());
|
||||
if (matchingIndex >= 1) {
|
||||
final matchingCategory = categories.data.removeAt(matchingIndex);
|
||||
categories.data.insert(0, matchingCategory);
|
||||
}
|
||||
return categories;
|
||||
})
|
||||
.asObservable();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:frosty/utils.dart';
|
|||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/photo_view.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:frosty/widgets/uptime.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
|
@ -72,7 +73,19 @@ class StreamCard extends StatelessWidget {
|
|||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
thumbnail,
|
||||
GestureDetector(
|
||||
onLongPress: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyPhotoViewDialog(
|
||||
imageUrl: streamInfo.thumbnailUrl.replaceFirst(
|
||||
'-{width}x{height}',
|
||||
'',
|
||||
) +
|
||||
cacheUrlExtension,
|
||||
),
|
||||
),
|
||||
child: thumbnail,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: const BoxDecoration(
|
||||
|
|
@ -173,6 +186,7 @@ class StreamCard extends StatelessWidget {
|
|||
style: TextStyle(
|
||||
fontSize: subFontSize,
|
||||
color: fontColor?.withOpacity(0.8),
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ class _StreamsListState extends State<StreamsList>
|
|||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ class _CategoriesState extends State<Categories>
|
|||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,8 +24,31 @@ class OnboardingLogin extends StatelessWidget {
|
|||
buttonIcon: const Icon(SimpleIcons.twitch),
|
||||
skipRoute: const OnboardingSetup(),
|
||||
route: Scaffold(
|
||||
appBar: const FrostyAppBar(
|
||||
title: Text('Connect with Twitch'),
|
||||
appBar: FrostyAppBar(
|
||||
title: const Text('Connect with Twitch'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_rounded),
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title:
|
||||
const Text('Workaround for the Twitch cookie banner'),
|
||||
content: const Text(
|
||||
'If the Twitch cookie banner is still blocking the login, try clicking one of the links in the cookie policy description and navigating until you reach the Twitch home page. From there, you can try logging in on the top right profile icon. Once logged in, go back to the first step of the onboarding and then try again.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: WebViewWidget(
|
||||
controller: authStore.createAuthWebViewController(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_performance/firebase_performance.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/onboarding/onboarding_scaffold.dart';
|
||||
|
|
@ -9,6 +10,7 @@ import 'package:frosty/screens/settings/stores/settings_store.dart';
|
|||
import 'package:frosty/screens/settings/widgets/settings_list_select.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OnboardingSetup extends StatelessWidget {
|
||||
const OnboardingSetup({super.key});
|
||||
|
|
@ -32,6 +34,37 @@ class OnboardingSetup extends StatelessWidget {
|
|||
onChanged: (newTheme) => settingsStore.themeType =
|
||||
ThemeType.values[themeNames.indexOf(newTheme)],
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show historical recent messages',
|
||||
subtitle: Text.rich(
|
||||
TextSpan(
|
||||
text:
|
||||
'Loads historical recent messages in chat through a third-party API service at ',
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'https://recent-messages.robotty.de/',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Colors.blue,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => launchUrl(
|
||||
Uri.parse(
|
||||
'https://recent-messages.robotty.de/',
|
||||
),
|
||||
mode: settingsStore.launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppBrowserView,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
value: settingsStore.showRecentMessages,
|
||||
onChanged: (newValue) =>
|
||||
settingsStore.showRecentMessages = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Share crash logs and analytics',
|
||||
subtitle: const Text(
|
||||
|
|
|
|||
|
|
@ -64,8 +64,32 @@ class ProfileCard extends StatelessWidget {
|
|||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: const FrostyAppBar(
|
||||
title: Text('Connect with Twitch'),
|
||||
appBar: FrostyAppBar(
|
||||
title: const Text('Connect with Twitch'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_rounded),
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
'Workaround for the Twitch cookie banner',
|
||||
),
|
||||
content: const Text(
|
||||
'If the Twitch cookie banner is still blocking the login, try clicking one of the links in the cookie policy description and navigating until you reach the Twitch home page. From there, you can try logging in on the top right profile icon. Once logged in, exit this page and then try again.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: WebViewWidget(
|
||||
controller: authStore.createAuthWebViewController(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
|
|
@ -9,6 +10,7 @@ import 'package:frosty/screens/settings/widgets/settings_list_slider.dart';
|
|||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ChatSettings extends StatefulWidget {
|
||||
final SettingsStore settingsStore;
|
||||
|
|
@ -170,17 +172,23 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
onChanged: (newValue) => settingsStore.timestampType =
|
||||
TimestampType.values[timestampNames.indexOf(newValue)],
|
||||
),
|
||||
const SectionHeader('Delay', showDivider: true),
|
||||
SettingsListSlider(
|
||||
title: 'Message delay',
|
||||
trailing: '${settingsStore.chatDelay.toInt()}s',
|
||||
subtitle:
|
||||
'Adds a delay (in seconds) before each message is rendered in chat. ${Platform.isIOS ? '15 seconds is recommended for iOS.' : ''}',
|
||||
value: settingsStore.chatDelay,
|
||||
max: 30.0,
|
||||
divisions: 30,
|
||||
onChanged: (newValue) => settingsStore.chatDelay = newValue,
|
||||
const SectionHeader('Delay and latency', showDivider: true),
|
||||
SettingsListSwitch(
|
||||
title: 'Sync message delay with stream latency (experimental)',
|
||||
value: settingsStore.autoSyncChatDelay,
|
||||
onChanged: (newValue) => settingsStore.autoSyncChatDelay = newValue,
|
||||
),
|
||||
if (!settingsStore.autoSyncChatDelay)
|
||||
SettingsListSlider(
|
||||
title: 'Message delay',
|
||||
trailing: '${settingsStore.chatDelay.toInt()} seconds',
|
||||
subtitle:
|
||||
'Adds a delay before each message is rendered in chat. ${Platform.isIOS ? '15 seconds is recommended for iOS.' : ''}',
|
||||
value: settingsStore.chatDelay,
|
||||
max: 30.0,
|
||||
divisions: 30,
|
||||
onChanged: (newValue) => settingsStore.chatDelay = newValue,
|
||||
),
|
||||
const SectionHeader('Alerts', showDivider: true),
|
||||
SettingsListSwitch(
|
||||
title: 'Highlight first time chatters',
|
||||
|
|
@ -282,6 +290,83 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
value: settingsStore.autocomplete,
|
||||
onChanged: (newValue) => settingsStore.autocomplete = newValue,
|
||||
),
|
||||
const SectionHeader('Emotes and badges', showDivider: true),
|
||||
SettingsListSwitch(
|
||||
title: 'Show Twitch emotes',
|
||||
value: settingsStore.showTwitchEmotes,
|
||||
onChanged: (newValue) => settingsStore.showTwitchEmotes = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show Twitch badges',
|
||||
value: settingsStore.showTwitchBadges,
|
||||
onChanged: (newValue) => settingsStore.showTwitchBadges = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show 7TV emotes',
|
||||
value: settingsStore.show7TVEmotes,
|
||||
onChanged: (newValue) => settingsStore.show7TVEmotes = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show BTTV emotes',
|
||||
value: settingsStore.showBTTVEmotes,
|
||||
onChanged: (newValue) => settingsStore.showBTTVEmotes = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show BTTV badges',
|
||||
value: settingsStore.showBTTVBadges,
|
||||
onChanged: (newValue) => settingsStore.showBTTVBadges = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show FFZ emotes',
|
||||
value: settingsStore.showFFZEmotes,
|
||||
onChanged: (newValue) => settingsStore.showFFZEmotes = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show FFZ badges',
|
||||
value: settingsStore.showFFZBadges,
|
||||
onChanged: (newValue) => settingsStore.showFFZBadges = newValue,
|
||||
),
|
||||
const SectionHeader('Recent messages', showDivider: true),
|
||||
SettingsListSwitch(
|
||||
title: 'Show historical recent messages',
|
||||
subtitle: Text.rich(
|
||||
TextSpan(
|
||||
text:
|
||||
'Loads historical recent messages in chat through a third-party API service at ',
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'https://recent-messages.robotty.de/',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Colors.blue,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => launchUrl(
|
||||
Uri.parse(
|
||||
'https://recent-messages.robotty.de/',
|
||||
),
|
||||
mode: settingsStore.launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppBrowserView,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
value: settingsStore.showRecentMessages,
|
||||
onChanged: (newValue) =>
|
||||
settingsStore.showRecentMessages = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Darken historical recent messages',
|
||||
subtitle: const Text(
|
||||
'Makes historical recent messages 50% opacity to distinguish them from live messages.',
|
||||
),
|
||||
value: settingsStore.darkenRecentMessages,
|
||||
onChanged: (newValue) =>
|
||||
settingsStore.darkenRecentMessages = newValue,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/release_notes.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
|
@ -70,7 +71,7 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
onTap: () async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
|
|
@ -81,18 +82,17 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
applicationName: packageInfo.appName,
|
||||
applicationVersion:
|
||||
'Version ${packageInfo.version} (${packageInfo.buildNumber})',
|
||||
applicationLegalese: '\u{a9} 2023 Tommy Chow',
|
||||
applicationLegalese: '\u{a9} 2024 Tommy Chow',
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.launch_rounded),
|
||||
title: const Text('Changelog'),
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse('https://github.com/tommyxchow/frosty/releases'),
|
||||
mode: widget.settingsStore.launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppWebView,
|
||||
leading: const Icon(Icons.notes_rounded),
|
||||
title: const Text('Release notes'),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ReleaseNotes(),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -102,7 +102,7 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
Uri.parse('https://www.frostyapp.io/#faq'),
|
||||
mode: widget.settingsStore.launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppWebView,
|
||||
: LaunchMode.inAppBrowserView,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -113,7 +113,8 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
|
||||
await DefaultCacheManager().emptyCache();
|
||||
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: AlertMessage(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class Settings extends StatelessWidget {
|
|||
Uri.parse('https://www.buymeacoffee.com/tommychow'),
|
||||
mode: settingsStore.launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppWebView,
|
||||
: LaunchMode.inAppBrowserView,
|
||||
),
|
||||
icon: const Icon(SimpleIcons.buymeacoffee),
|
||||
),
|
||||
|
|
@ -43,7 +43,7 @@ class Settings extends StatelessWidget {
|
|||
Uri.parse('https://github.com/tommyxchow/frosty'),
|
||||
mode: settingsStore.launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppWebView,
|
||||
: LaunchMode.inAppBrowserView,
|
||||
),
|
||||
icon: const Icon(SimpleIcons.github),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -84,12 +84,42 @@ abstract class AuthBase with Store {
|
|||
}
|
||||
|
||||
WebViewController createAuthWebViewController({Widget? routeAfter}) {
|
||||
return WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
final webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted);
|
||||
|
||||
return webViewController
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onNavigationRequest: (request) =>
|
||||
handleNavigation(request: request, routeAfter: routeAfter),
|
||||
onPageFinished: (_) => webViewController.runJavaScript(
|
||||
'''
|
||||
{
|
||||
function modifyElement(element) {
|
||||
element.style.maxHeight = '20vh';
|
||||
element.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (let mutation of mutations) {
|
||||
if (mutation.type === 'childList') {
|
||||
const element = document.querySelector('.fAVISI');
|
||||
if (element) {
|
||||
modifyElement(element);
|
||||
observer.disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
''',
|
||||
),
|
||||
),
|
||||
)
|
||||
..loadRequest(
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
static const defaultTimestampType = TimestampType.disabled;
|
||||
|
||||
// Delay defaults
|
||||
static const defaultAutoSyncChatDelay = false;
|
||||
static const defaultChatDelay = 0.0;
|
||||
|
||||
// Alert defaults
|
||||
|
|
@ -131,7 +132,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
static const defaultLandscapeChatLeftSide = false;
|
||||
static const defaultLandscapeForceVerticalChat = false;
|
||||
static const defaultLandscapeCutout = LandscapeCutoutType.none;
|
||||
static const defaultChatWidth = 0.3;
|
||||
static const defaultChatWidth = 0.25;
|
||||
static const defaultFullScreenChatOverlayOpacity = 0.5;
|
||||
|
||||
// Sleep defaults
|
||||
|
|
@ -140,6 +141,19 @@ abstract class _SettingsStoreBase with Store {
|
|||
// Autocomplete defaults
|
||||
static const defaultAutocomplete = true;
|
||||
|
||||
// Emotes and badges defaults
|
||||
static const defaultShowTwitchEmotes = true;
|
||||
static const defaultShowTwitchBadges = true;
|
||||
static const defaultShow7TVEmotes = true;
|
||||
static const defaultShowBTTVEmotes = true;
|
||||
static const defaultShowBTTVBadges = true;
|
||||
static const defaultShowFFZEmotes = true;
|
||||
static const defaultShowFFZBadges = true;
|
||||
|
||||
// Recent messages defaults
|
||||
static const defaultShowRecentMessages = false;
|
||||
static const defaultDarkenRecentMessages = true;
|
||||
|
||||
// Message sizing options
|
||||
@JsonKey(defaultValue: defaultBadgeScale)
|
||||
@observable
|
||||
|
|
@ -182,6 +196,10 @@ abstract class _SettingsStoreBase with Store {
|
|||
var timestampType = defaultTimestampType;
|
||||
|
||||
// Delay options
|
||||
@JsonKey(defaultValue: defaultAutoSyncChatDelay)
|
||||
@observable
|
||||
var autoSyncChatDelay = defaultAutoSyncChatDelay;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatDelay)
|
||||
@observable
|
||||
var chatDelay = defaultChatDelay;
|
||||
|
|
@ -239,6 +257,44 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var autocomplete = defaultAutocomplete;
|
||||
|
||||
// Emotes and badges
|
||||
@JsonKey(defaultValue: defaultShowTwitchEmotes)
|
||||
@observable
|
||||
var showTwitchEmotes = defaultShowTwitchEmotes;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowTwitchBadges)
|
||||
@observable
|
||||
var showTwitchBadges = defaultShowTwitchBadges;
|
||||
|
||||
@JsonKey(defaultValue: defaultShow7TVEmotes)
|
||||
@observable
|
||||
var show7TVEmotes = defaultShow7TVEmotes;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowBTTVEmotes)
|
||||
@observable
|
||||
var showBTTVEmotes = defaultShowBTTVEmotes;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowBTTVBadges)
|
||||
@observable
|
||||
var showBTTVBadges = defaultShowBTTVBadges;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowFFZEmotes)
|
||||
@observable
|
||||
var showFFZEmotes = defaultShowFFZEmotes;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowFFZBadges)
|
||||
@observable
|
||||
var showFFZBadges = defaultShowFFZBadges;
|
||||
|
||||
// Recent messages
|
||||
@JsonKey(defaultValue: defaultShowRecentMessages)
|
||||
@observable
|
||||
var showRecentMessages = defaultShowRecentMessages;
|
||||
|
||||
@JsonKey(defaultValue: defaultDarkenRecentMessages)
|
||||
@observable
|
||||
var darkenRecentMessages = defaultDarkenRecentMessages;
|
||||
|
||||
@action
|
||||
void resetChatSettings() {
|
||||
badgeScale = defaultBadgeScale;
|
||||
|
|
@ -252,6 +308,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
showChatMessageDividers = defaultShowChatMessageDividers;
|
||||
timestampType = defaultTimestampType;
|
||||
|
||||
autoSyncChatDelay = defaultAutoSyncChatDelay;
|
||||
chatDelay = defaultChatDelay;
|
||||
|
||||
highlightFirstTimeChatter = defaultHighlightFirstTimeChatter;
|
||||
|
|
@ -269,6 +326,17 @@ abstract class _SettingsStoreBase with Store {
|
|||
|
||||
chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
|
||||
autocomplete = defaultAutocomplete;
|
||||
|
||||
showTwitchEmotes = defaultShowTwitchEmotes;
|
||||
showTwitchBadges = defaultShowTwitchBadges;
|
||||
show7TVEmotes = defaultShow7TVEmotes;
|
||||
showBTTVEmotes = defaultShowBTTVEmotes;
|
||||
showBTTVBadges = defaultShowBTTVBadges;
|
||||
showFFZEmotes = defaultShowFFZEmotes;
|
||||
showFFZBadges = defaultShowFFZBadges;
|
||||
|
||||
showRecentMessages = defaultShowRecentMessages;
|
||||
darkenRecentMessages = defaultDarkenRecentMessages;
|
||||
}
|
||||
|
||||
// * Other settings
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
_$TimestampTypeEnumMap, json['timestampType'],
|
||||
unknownValue: TimestampType.disabled) ??
|
||||
TimestampType.disabled
|
||||
..autoSyncChatDelay = json['autoSyncChatDelay'] as bool? ?? false
|
||||
..chatDelay = (json['chatDelay'] as num?)?.toDouble() ?? 0.0
|
||||
..highlightFirstTimeChatter =
|
||||
json['highlightFirstTimeChatter'] as bool? ?? true
|
||||
|
|
@ -48,11 +49,20 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
..landscapeCutout = $enumDecodeNullable(
|
||||
_$LandscapeCutoutTypeEnumMap, json['landscapeCutout']) ??
|
||||
LandscapeCutoutType.none
|
||||
..chatWidth = (json['chatWidth'] as num?)?.toDouble() ?? 0.3
|
||||
..chatWidth = (json['chatWidth'] as num?)?.toDouble() ?? 0.25
|
||||
..fullScreenChatOverlayOpacity =
|
||||
(json['fullScreenChatOverlayOpacity'] as num?)?.toDouble() ?? 0.5
|
||||
..chatOnlyPreventSleep = json['chatOnlyPreventSleep'] as bool? ?? false
|
||||
..autocomplete = json['autocomplete'] as bool? ?? true
|
||||
..showTwitchEmotes = json['showTwitchEmotes'] as bool? ?? true
|
||||
..showTwitchBadges = json['showTwitchBadges'] as bool? ?? true
|
||||
..show7TVEmotes = json['show7TVEmotes'] as bool? ?? true
|
||||
..showBTTVEmotes = json['showBTTVEmotes'] as bool? ?? true
|
||||
..showBTTVBadges = json['showBTTVBadges'] as bool? ?? true
|
||||
..showFFZEmotes = json['showFFZEmotes'] as bool? ?? true
|
||||
..showFFZBadges = json['showFFZBadges'] as bool? ?? true
|
||||
..showRecentMessages = json['showRecentMessages'] as bool? ?? false
|
||||
..darkenRecentMessages = json['darkenRecentMessages'] as bool? ?? true
|
||||
..shareCrashLogsAndAnalytics =
|
||||
json['shareCrashLogsAndAnalytics'] as bool? ?? true
|
||||
..fullScreen = json['fullScreen'] as bool? ?? false
|
||||
|
|
@ -79,6 +89,7 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'showDeletedMessages': instance.showDeletedMessages,
|
||||
'showChatMessageDividers': instance.showChatMessageDividers,
|
||||
'timestampType': _$TimestampTypeEnumMap[instance.timestampType]!,
|
||||
'autoSyncChatDelay': instance.autoSyncChatDelay,
|
||||
'chatDelay': instance.chatDelay,
|
||||
'highlightFirstTimeChatter': instance.highlightFirstTimeChatter,
|
||||
'showUserNotices': instance.showUserNotices,
|
||||
|
|
@ -93,6 +104,15 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'fullScreenChatOverlayOpacity': instance.fullScreenChatOverlayOpacity,
|
||||
'chatOnlyPreventSleep': instance.chatOnlyPreventSleep,
|
||||
'autocomplete': instance.autocomplete,
|
||||
'showTwitchEmotes': instance.showTwitchEmotes,
|
||||
'showTwitchBadges': instance.showTwitchBadges,
|
||||
'show7TVEmotes': instance.show7TVEmotes,
|
||||
'showBTTVEmotes': instance.showBTTVEmotes,
|
||||
'showBTTVBadges': instance.showBTTVBadges,
|
||||
'showFFZEmotes': instance.showFFZEmotes,
|
||||
'showFFZBadges': instance.showFFZBadges,
|
||||
'showRecentMessages': instance.showRecentMessages,
|
||||
'darkenRecentMessages': instance.darkenRecentMessages,
|
||||
'shareCrashLogsAndAnalytics': instance.shareCrashLogsAndAnalytics,
|
||||
'fullScreen': instance.fullScreen,
|
||||
'fullScreenChatOverlay': instance.fullScreenChatOverlay,
|
||||
|
|
@ -431,6 +451,22 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$autoSyncChatDelayAtom =
|
||||
Atom(name: '_SettingsStoreBase.autoSyncChatDelay', context: context);
|
||||
|
||||
@override
|
||||
bool get autoSyncChatDelay {
|
||||
_$autoSyncChatDelayAtom.reportRead();
|
||||
return super.autoSyncChatDelay;
|
||||
}
|
||||
|
||||
@override
|
||||
set autoSyncChatDelay(bool value) {
|
||||
_$autoSyncChatDelayAtom.reportWrite(value, super.autoSyncChatDelay, () {
|
||||
super.autoSyncChatDelay = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$chatDelayAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatDelay', context: context);
|
||||
|
||||
|
|
@ -647,6 +683,151 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$showTwitchEmotesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showTwitchEmotes', context: context);
|
||||
|
||||
@override
|
||||
bool get showTwitchEmotes {
|
||||
_$showTwitchEmotesAtom.reportRead();
|
||||
return super.showTwitchEmotes;
|
||||
}
|
||||
|
||||
@override
|
||||
set showTwitchEmotes(bool value) {
|
||||
_$showTwitchEmotesAtom.reportWrite(value, super.showTwitchEmotes, () {
|
||||
super.showTwitchEmotes = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showTwitchBadgesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showTwitchBadges', context: context);
|
||||
|
||||
@override
|
||||
bool get showTwitchBadges {
|
||||
_$showTwitchBadgesAtom.reportRead();
|
||||
return super.showTwitchBadges;
|
||||
}
|
||||
|
||||
@override
|
||||
set showTwitchBadges(bool value) {
|
||||
_$showTwitchBadgesAtom.reportWrite(value, super.showTwitchBadges, () {
|
||||
super.showTwitchBadges = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$show7TVEmotesAtom =
|
||||
Atom(name: '_SettingsStoreBase.show7TVEmotes', context: context);
|
||||
|
||||
@override
|
||||
bool get show7TVEmotes {
|
||||
_$show7TVEmotesAtom.reportRead();
|
||||
return super.show7TVEmotes;
|
||||
}
|
||||
|
||||
@override
|
||||
set show7TVEmotes(bool value) {
|
||||
_$show7TVEmotesAtom.reportWrite(value, super.show7TVEmotes, () {
|
||||
super.show7TVEmotes = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showBTTVEmotesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showBTTVEmotes', context: context);
|
||||
|
||||
@override
|
||||
bool get showBTTVEmotes {
|
||||
_$showBTTVEmotesAtom.reportRead();
|
||||
return super.showBTTVEmotes;
|
||||
}
|
||||
|
||||
@override
|
||||
set showBTTVEmotes(bool value) {
|
||||
_$showBTTVEmotesAtom.reportWrite(value, super.showBTTVEmotes, () {
|
||||
super.showBTTVEmotes = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showBTTVBadgesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showBTTVBadges', context: context);
|
||||
|
||||
@override
|
||||
bool get showBTTVBadges {
|
||||
_$showBTTVBadgesAtom.reportRead();
|
||||
return super.showBTTVBadges;
|
||||
}
|
||||
|
||||
@override
|
||||
set showBTTVBadges(bool value) {
|
||||
_$showBTTVBadgesAtom.reportWrite(value, super.showBTTVBadges, () {
|
||||
super.showBTTVBadges = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showFFZEmotesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showFFZEmotes', context: context);
|
||||
|
||||
@override
|
||||
bool get showFFZEmotes {
|
||||
_$showFFZEmotesAtom.reportRead();
|
||||
return super.showFFZEmotes;
|
||||
}
|
||||
|
||||
@override
|
||||
set showFFZEmotes(bool value) {
|
||||
_$showFFZEmotesAtom.reportWrite(value, super.showFFZEmotes, () {
|
||||
super.showFFZEmotes = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showFFZBadgesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showFFZBadges', context: context);
|
||||
|
||||
@override
|
||||
bool get showFFZBadges {
|
||||
_$showFFZBadgesAtom.reportRead();
|
||||
return super.showFFZBadges;
|
||||
}
|
||||
|
||||
@override
|
||||
set showFFZBadges(bool value) {
|
||||
_$showFFZBadgesAtom.reportWrite(value, super.showFFZBadges, () {
|
||||
super.showFFZBadges = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showRecentMessagesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showRecentMessages', context: context);
|
||||
|
||||
@override
|
||||
bool get showRecentMessages {
|
||||
_$showRecentMessagesAtom.reportRead();
|
||||
return super.showRecentMessages;
|
||||
}
|
||||
|
||||
@override
|
||||
set showRecentMessages(bool value) {
|
||||
_$showRecentMessagesAtom.reportWrite(value, super.showRecentMessages, () {
|
||||
super.showRecentMessages = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$darkenRecentMessagesAtom =
|
||||
Atom(name: '_SettingsStoreBase.darkenRecentMessages', context: context);
|
||||
|
||||
@override
|
||||
bool get darkenRecentMessages {
|
||||
_$darkenRecentMessagesAtom.reportRead();
|
||||
return super.darkenRecentMessages;
|
||||
}
|
||||
|
||||
@override
|
||||
set darkenRecentMessages(bool value) {
|
||||
_$darkenRecentMessagesAtom.reportWrite(value, super.darkenRecentMessages,
|
||||
() {
|
||||
super.darkenRecentMessages = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$shareCrashLogsAndAnalyticsAtom = Atom(
|
||||
name: '_SettingsStoreBase.shareCrashLogsAndAnalytics', context: context);
|
||||
|
||||
|
|
@ -788,6 +969,7 @@ useReadableColors: ${useReadableColors},
|
|||
showDeletedMessages: ${showDeletedMessages},
|
||||
showChatMessageDividers: ${showChatMessageDividers},
|
||||
timestampType: ${timestampType},
|
||||
autoSyncChatDelay: ${autoSyncChatDelay},
|
||||
chatDelay: ${chatDelay},
|
||||
highlightFirstTimeChatter: ${highlightFirstTimeChatter},
|
||||
showUserNotices: ${showUserNotices},
|
||||
|
|
@ -801,6 +983,15 @@ chatWidth: ${chatWidth},
|
|||
fullScreenChatOverlayOpacity: ${fullScreenChatOverlayOpacity},
|
||||
chatOnlyPreventSleep: ${chatOnlyPreventSleep},
|
||||
autocomplete: ${autocomplete},
|
||||
showTwitchEmotes: ${showTwitchEmotes},
|
||||
showTwitchBadges: ${showTwitchBadges},
|
||||
show7TVEmotes: ${show7TVEmotes},
|
||||
showBTTVEmotes: ${showBTTVEmotes},
|
||||
showBTTVBadges: ${showBTTVBadges},
|
||||
showFFZEmotes: ${showFFZEmotes},
|
||||
showFFZBadges: ${showFFZBadges},
|
||||
showRecentMessages: ${showRecentMessages},
|
||||
darkenRecentMessages: ${darkenRecentMessages},
|
||||
shareCrashLogsAndAnalytics: ${shareCrashLogsAndAnalytics},
|
||||
fullScreen: ${fullScreen},
|
||||
fullScreenChatOverlay: ${fullScreenChatOverlay}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_slider.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class VideoSettings extends StatelessWidget {
|
||||
|
|
@ -23,7 +24,7 @@ class VideoSettings extends StatelessWidget {
|
|||
value: settingsStore.showVideo,
|
||||
onChanged: (newValue) => settingsStore.showVideo = newValue,
|
||||
),
|
||||
if (!Platform.isIOS)
|
||||
if (!Platform.isIOS || isIPad())
|
||||
SettingsListSwitch(
|
||||
title: 'Default to highest quality',
|
||||
value: settingsStore.defaultToHighestQuality,
|
||||
|
|
|
|||
68
lib/screens/settings/widgets/release_notes.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class ReleaseNotes extends StatefulWidget {
|
||||
const ReleaseNotes({super.key});
|
||||
|
||||
@override
|
||||
State<ReleaseNotes> createState() => _ReleaseNotesState();
|
||||
}
|
||||
|
||||
class _ReleaseNotesState extends State<ReleaseNotes> {
|
||||
String releaseNotes = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
rootBundle.loadString('assets/release-notes.md').then(
|
||||
(changelog) {
|
||||
setState(() {
|
||||
releaseNotes = changelog;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Release notes'),
|
||||
),
|
||||
body: Markdown(
|
||||
data: releaseNotes,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
h2: const TextStyle(fontSize: 20),
|
||||
h3: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
h3Padding: const EdgeInsets.only(top: 16),
|
||||
h4: const TextStyle(fontSize: 14),
|
||||
h4Padding: const EdgeInsets.only(top: 16),
|
||||
p: const TextStyle(fontSize: 14),
|
||||
horizontalRuleDecoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapLink: (text, href, title) {
|
||||
if (href != null) {
|
||||
launchUrlString(
|
||||
href,
|
||||
mode: context.read<SettingsStore>().launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppBrowserView,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
|
||||
String getReadableName(String displayName, String username) {
|
||||
|
|
@ -7,3 +10,20 @@ String getReadableName(String displayName, String username) {
|
|||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
var _isIPad = false;
|
||||
|
||||
bool isIPad() {
|
||||
return _isIPad;
|
||||
}
|
||||
|
||||
Future<void> initUtils() async {
|
||||
// Determine whether the device is an iPad or not.
|
||||
if (Platform.isIOS) {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final info = await deviceInfo.iosInfo;
|
||||
if (info.model.toLowerCase().contains('ipad')) {
|
||||
_isIPad = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,15 @@ class BlockReportModal extends StatelessWidget {
|
|||
leading: const Icon(Icons.block_rounded),
|
||||
onTap: () => authStore
|
||||
.showBlockDialog(
|
||||
context,
|
||||
targetUser: name,
|
||||
targetUserId: userId,
|
||||
)
|
||||
.then((_) => Navigator.pop(context)),
|
||||
context,
|
||||
targetUser: name,
|
||||
targetUserId: userId,
|
||||
)
|
||||
.then((_) {
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}),
|
||||
title: Text('Block $name'),
|
||||
),
|
||||
ListTile(
|
||||
|
|
|
|||
43
lib/widgets/photo_view.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
class FrostyPhotoViewDialog extends StatefulWidget {
|
||||
final String imageUrl;
|
||||
|
||||
const FrostyPhotoViewDialog({super.key, required this.imageUrl});
|
||||
|
||||
@override
|
||||
State<FrostyPhotoViewDialog> createState() => _FrostyPhotoViewDialogState();
|
||||
}
|
||||
|
||||
class _FrostyPhotoViewDialogState extends State<FrostyPhotoViewDialog> {
|
||||
PhotoViewScaleState photoViewScaleState = PhotoViewScaleState.initial;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Dismissible(
|
||||
key: const Key('photo_view_dismissible'),
|
||||
direction: photoViewScaleState == PhotoViewScaleState.initial
|
||||
? DismissDirection.vertical
|
||||
: DismissDirection.none,
|
||||
onDismissed: Navigator.of(context).pop,
|
||||
child: PhotoView(
|
||||
imageProvider: CachedNetworkImageProvider(widget.imageUrl),
|
||||
scaleStateChangedCallback: (value) =>
|
||||
setState(() => photoViewScaleState = value),
|
||||
backgroundDecoration:
|
||||
const BoxDecoration(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
|
|||
539
pubspec.lock
473
pubspec.yaml
|
|
@ -1,229 +1,244 @@
|
|||
name: frosty
|
||||
description: A Twitch client for iOS and Android.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 4.2.1+47
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
json_annotation: ^4.6.0
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
http: ^1.1.0
|
||||
web_socket_channel: ^2.1.0
|
||||
flutter_secure_storage: ^9.0.0
|
||||
intl: ^0.18.1
|
||||
cached_network_image: ^3.1.0
|
||||
webview_flutter: ^4.2.0
|
||||
provider: ^6.0.1
|
||||
mobx: ^2.0.7+4
|
||||
flutter_mobx: ^2.0.0
|
||||
shared_preferences: ^2.0.8
|
||||
flutter_svg: ^2.0.5
|
||||
url_launcher: ^6.0.17
|
||||
package_info_plus: ^5.0.1
|
||||
collection: ^1.16.0
|
||||
flutter_cache_manager: ^3.3.0
|
||||
device_info_plus: ^9.0.3
|
||||
wakelock_plus: ^1.1.2
|
||||
simple_icons: ^7.10.0
|
||||
simple_pip_mode:
|
||||
git:
|
||||
url: https://github.com/tommyxchow/simple_pip_mode_flutter
|
||||
firebase_core: ^2.10.0
|
||||
firebase_crashlytics: ^3.1.2
|
||||
firebase_performance: ^0.9.1+1
|
||||
firebase_analytics: ^10.2.1
|
||||
advanced_in_app_review: ^1.1.3
|
||||
webview_flutter_android: ^3.7.0
|
||||
webview_flutter_wkwebview: ^3.4.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^3.0.0
|
||||
build_runner: ^2.1.1
|
||||
json_serializable: ^6.0.0
|
||||
mobx_codegen: ^2.0.6
|
||||
flutter_native_splash: ^2.0.5
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
fonts:
|
||||
- family: Inter
|
||||
fonts:
|
||||
- asset: assets/fonts/Inter-Thin.ttf
|
||||
- asset: assets/fonts/Inter-ExtraLight.ttf
|
||||
- asset: assets/fonts/Inter-Light.ttf
|
||||
- asset: assets/fonts/Inter-Regular.ttf
|
||||
- asset: assets/fonts/Inter-Medium.ttf
|
||||
- asset: assets/fonts/Inter-SemiBold.ttf
|
||||
- asset: assets/fonts/Inter-Bold.ttf
|
||||
- asset: assets/fonts/Inter-ExtraBold.ttf
|
||||
- asset: assets/fonts/Inter-Black.ttf
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
assets:
|
||||
- assets/icons/logo.svg
|
||||
- assets/fonts/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
flutter_native_splash:
|
||||
# This package generates native code to customize Flutter's default white native splash screen
|
||||
# with background color and splash image.
|
||||
# Customize the parameters below, and run the following command in the terminal:
|
||||
# flutter pub run flutter_native_splash:create
|
||||
# To restore Flutter's default white splash screen, run the following command in the terminal:
|
||||
# flutter pub run flutter_native_splash:remove
|
||||
|
||||
# color or background_image is the only required parameter. Use color to set the background
|
||||
# of your splash screen to a solid color. Use background_image to set the background of your
|
||||
# splash screen to a png image. This is useful for gradients. The image will be stretch to the
|
||||
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
|
||||
# color: "#000000"
|
||||
background_image: "assets/icons/bg.png"
|
||||
|
||||
# Optional parameters are listed below. To enable a parameter, uncomment the line by removing
|
||||
# the leading # character.
|
||||
|
||||
# The image parameter allows you to specify an image used in the splash screen. It must be a
|
||||
# png file and should be sized for 4x pixel density.
|
||||
image: assets/icons/logo.png
|
||||
|
||||
# This property allows you to specify an image used as branding in the splash screen. It must be
|
||||
# a png file. Currently, it is only supported for Android and iOS.
|
||||
# branding: assets/dart.png
|
||||
|
||||
# Specify your branding image for dark mode.
|
||||
#branding_dark: assets/dart_dark.png
|
||||
|
||||
# To position the branding image at the bottom of the screen you can use bottom, bottomRight,
|
||||
# and bottomLeft. The default values is bottom if not specified or specified something else.
|
||||
#
|
||||
# Make sure this content mode value should not be similar to android_gravity value and ios_content_mode
|
||||
# value.
|
||||
# branding_mode: bottom
|
||||
|
||||
# The color_dark, background_image_dark, and image_dark are parameters that set the background
|
||||
# and image when the device is in dark mode. If they are not specified, the app will use the
|
||||
# parameters from above. If the image_dark parameter is specified, color_dark or
|
||||
# background_image_dark must be specified. color_dark and background_image_dark cannot both be
|
||||
# set.
|
||||
#color_dark: "#042a49"
|
||||
#background_image_dark: "assets/dark-background.png"
|
||||
#image_dark: assets/splash-invert.png
|
||||
|
||||
# The android, ios and web parameters can be used to disable generating a splash screen on a given
|
||||
# platform.
|
||||
#android: false
|
||||
#ios: false
|
||||
#web: false
|
||||
|
||||
# The position of the splash image can be set with android_gravity, ios_content_mode, and
|
||||
# web_image_mode parameters. All default to center.
|
||||
#
|
||||
# android_gravity can be one of the following Android Gravity (see
|
||||
# https://developer.android.com/reference/android/view/Gravity): bottom, center,
|
||||
# center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal,
|
||||
# fill_vertical, left, right, start, or top.
|
||||
#android_gravity: center
|
||||
#
|
||||
# ios_content_mode can be one of the following iOS UIView.ContentMode (see
|
||||
# https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill,
|
||||
# scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight,
|
||||
# bottomLeft, or bottomRight.
|
||||
#ios_content_mode: center
|
||||
#
|
||||
# web_image_mode can be one of the following modes: center, contain, stretch, and cover.
|
||||
#web_image_mode: center
|
||||
|
||||
# To hide the notification bar, use the fullscreen parameter. Has no affect in web since web
|
||||
# has no notification bar. Defaults to false.
|
||||
# NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads.
|
||||
# To show the notification bar, add the following code to your Flutter app:
|
||||
# WidgetsFlutterBinding.ensureInitialized();
|
||||
# SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||
#fullscreen: true
|
||||
|
||||
# If you have changed the name(s) of your info.plist file(s), you can specify the filename(s)
|
||||
# with the info_plist_files parameter. Remove only the # characters in the three lines below,
|
||||
# do not remove any spaces:
|
||||
#info_plist_files:
|
||||
# - 'ios/Runner/Info-Debug.plist'
|
||||
# - 'ios/Runner/Info-Release.plist'
|
||||
|
||||
flutter_launcher_icons:
|
||||
image_path_android: "assets/icons/icon_android.png"
|
||||
image_path_ios: "assets/icons/icon_ios.png"
|
||||
android: true
|
||||
ios: true
|
||||
remove_alpha_ios: true
|
||||
name: frosty
|
||||
description: A Twitch client for iOS and Android.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 4.5.0+51
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.0
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
json_annotation: ^4.6.0
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
http: ^1.1.0
|
||||
web_socket_channel: ^3.0.0
|
||||
flutter_secure_storage: ^9.0.0
|
||||
intl: ^0.19.0
|
||||
cached_network_image: ^3.1.0
|
||||
webview_flutter: ^4.2.0
|
||||
provider: ^6.0.1
|
||||
mobx: ^2.0.7+4
|
||||
flutter_mobx: ^2.0.0
|
||||
shared_preferences: ^2.0.8
|
||||
flutter_svg: ^2.0.5
|
||||
url_launcher: ^6.0.17
|
||||
package_info_plus: ^8.0.0
|
||||
collection: ^1.16.0
|
||||
flutter_cache_manager: ^3.3.0
|
||||
device_info_plus: ^10.1.0
|
||||
wakelock_plus: ^1.1.2
|
||||
simple_icons: ^10.1.3
|
||||
simple_pip_mode:
|
||||
git:
|
||||
url: https://github.com/tommyxchow/simple_pip_mode_flutter
|
||||
firebase_core: ^2.10.0
|
||||
firebase_crashlytics: ^3.1.2
|
||||
firebase_performance: ^0.9.1+1
|
||||
firebase_analytics: ^10.2.1
|
||||
advanced_in_app_review: ^1.1.3
|
||||
webview_flutter_android: ^3.7.0
|
||||
webview_flutter_wkwebview: ^3.4.3
|
||||
photo_view: ^0.15.0
|
||||
flutter_markdown: ^0.7.3+1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^4.0.0
|
||||
build_runner: ^2.1.1
|
||||
json_serializable: ^6.0.0
|
||||
mobx_codegen: ^2.0.6
|
||||
flutter_native_splash: ^2.0.5
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
fonts:
|
||||
- family: Inter
|
||||
fonts:
|
||||
- asset: assets/fonts/Inter-Thin.ttf
|
||||
- asset: assets/fonts/Inter-ExtraLight.ttf
|
||||
- asset: assets/fonts/Inter-Light.ttf
|
||||
- asset: assets/fonts/Inter-Regular.ttf
|
||||
- asset: assets/fonts/Inter-Medium.ttf
|
||||
- asset: assets/fonts/Inter-SemiBold.ttf
|
||||
- asset: assets/fonts/Inter-Bold.ttf
|
||||
- asset: assets/fonts/Inter-ExtraBold.ttf
|
||||
- asset: assets/fonts/Inter-Black.ttf
|
||||
- asset: assets/fonts/Inter-ThinItalic.ttf
|
||||
- asset: assets/fonts/Inter-ExtraLightItalic.ttf
|
||||
- asset: assets/fonts/Inter-LightItalic.ttf
|
||||
- asset: assets/fonts/Inter-Italic.ttf
|
||||
- asset: assets/fonts/Inter-MediumItalic.ttf
|
||||
- asset: assets/fonts/Inter-SemiBoldItalic.ttf
|
||||
- asset: assets/fonts/Inter-BoldItalic.ttf
|
||||
- asset: assets/fonts/Inter-ExtraBoldItalic.ttf
|
||||
- asset: assets/fonts/Inter-BlackItalic.ttf
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
assets:
|
||||
- assets/icons/logo.svg
|
||||
- assets/fonts/
|
||||
- assets/release-notes.md
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
|
||||
flutter_native_splash:
|
||||
# This package generates native code to customize Flutter's default white native splash screen
|
||||
# with background color and splash image.
|
||||
# Customize the parameters below, and run the following command in the terminal:
|
||||
# flutter pub run flutter_native_splash:create
|
||||
# To restore Flutter's default white splash screen, run the following command in the terminal:
|
||||
# flutter pub run flutter_native_splash:remove
|
||||
|
||||
# color or background_image is the only required parameter. Use color to set the background
|
||||
# of your splash screen to a solid color. Use background_image to set the background of your
|
||||
# splash screen to a png image. This is useful for gradients. The image will be stretch to the
|
||||
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
|
||||
# color: "#000000"
|
||||
background_image: "assets/icons/bg.png"
|
||||
|
||||
# Optional parameters are listed below. To enable a parameter, uncomment the line by removing
|
||||
# the leading # character.
|
||||
|
||||
# The image parameter allows you to specify an image used in the splash screen. It must be a
|
||||
# png file and should be sized for 4x pixel density.
|
||||
image: assets/icons/logo.png
|
||||
|
||||
# This property allows you to specify an image used as branding in the splash screen. It must be
|
||||
# a png file. Currently, it is only supported for Android and iOS.
|
||||
# branding: assets/dart.png
|
||||
|
||||
# Specify your branding image for dark mode.
|
||||
#branding_dark: assets/dart_dark.png
|
||||
|
||||
# To position the branding image at the bottom of the screen you can use bottom, bottomRight,
|
||||
# and bottomLeft. The default values is bottom if not specified or specified something else.
|
||||
#
|
||||
# Make sure this content mode value should not be similar to android_gravity value and ios_content_mode
|
||||
# value.
|
||||
# branding_mode: bottom
|
||||
|
||||
# The color_dark, background_image_dark, and image_dark are parameters that set the background
|
||||
# and image when the device is in dark mode. If they are not specified, the app will use the
|
||||
# parameters from above. If the image_dark parameter is specified, color_dark or
|
||||
# background_image_dark must be specified. color_dark and background_image_dark cannot both be
|
||||
# set.
|
||||
#color_dark: "#042a49"
|
||||
#background_image_dark: "assets/dark-background.png"
|
||||
#image_dark: assets/splash-invert.png
|
||||
|
||||
# The android, ios and web parameters can be used to disable generating a splash screen on a given
|
||||
# platform.
|
||||
#android: false
|
||||
#ios: false
|
||||
#web: false
|
||||
|
||||
# The position of the splash image can be set with android_gravity, ios_content_mode, and
|
||||
# web_image_mode parameters. All default to center.
|
||||
#
|
||||
# android_gravity can be one of the following Android Gravity (see
|
||||
# https://developer.android.com/reference/android/view/Gravity): bottom, center,
|
||||
# center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal,
|
||||
# fill_vertical, left, right, start, or top.
|
||||
#android_gravity: center
|
||||
#
|
||||
# ios_content_mode can be one of the following iOS UIView.ContentMode (see
|
||||
# https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill,
|
||||
# scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight,
|
||||
# bottomLeft, or bottomRight.
|
||||
#ios_content_mode: center
|
||||
#
|
||||
# web_image_mode can be one of the following modes: center, contain, stretch, and cover.
|
||||
#web_image_mode: center
|
||||
|
||||
# To hide the notification bar, use the fullscreen parameter. Has no affect in web since web
|
||||
# has no notification bar. Defaults to false.
|
||||
# NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads.
|
||||
# To show the notification bar, add the following code to your Flutter app:
|
||||
# WidgetsFlutterBinding.ensureInitialized();
|
||||
# SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||
#fullscreen: true
|
||||
|
||||
# If you have changed the name(s) of your info.plist file(s), you can specify the filename(s)
|
||||
# with the info_plist_files parameter. Remove only the # characters in the three lines below,
|
||||
# do not remove any spaces:
|
||||
#info_plist_files:
|
||||
# - 'ios/Runner/Info-Debug.plist'
|
||||
# - 'ios/Runner/Info-Release.plist'
|
||||
|
||||
flutter_launcher_icons:
|
||||
image_path_android: "assets/icons/icon_android.png"
|
||||
image_path_ios: "assets/icons/icon_ios.png"
|
||||
android: true
|
||||
ios: true
|
||||
remove_alpha_ios: true
|
||||
|
|
|
|||