Compare commits
112 Commits
Author | SHA1 | Date |
---|---|---|
Filip Krawczyk | 0a95fefba0 | |
Filip Krawczyk | b7cd985ba2 | |
Filip Krawczyk | c002632e15 | |
Filip Krawczyk | d2a2190151 | |
Filip Krawczyk | 8a9ce69c5a | |
Filip Krawczyk | eacbda0b5d | |
Filip Krawczyk | 9ff62f86c5 | |
Filip Krawczyk | 2d7d2a64bd | |
Filip Krawczyk | c15cfdf02c | |
Filip Krawczyk | 87726f283b | |
Filip Krawczyk | 8aefdfbf27 | |
Filip Krawczyk | b2ef0883e3 | |
Filip Krawczyk | e3b561835b | |
Filip Krawczyk | 9ec5410273 | |
Filip Krawczyk | d11a46393b | |
Filip Krawczyk | c83e93c755 | |
Filip Krawczyk | 2cc82e6a45 | |
Filip Krawczyk | 90553794b2 | |
Filip Krawczyk | b39d6b06d7 | |
Filip Krawczyk | 6729a040ea | |
Filip Krawczyk | cc8441dabc | |
Filip Krawczyk | 55daacf221 | |
Filip Krawczyk | 12084a3421 | |
Filip Krawczyk | cda72a1174 | |
Filip Krawczyk | 821558314e | |
Filip Krawczyk | ecb95d3bdb | |
Filip Krawczyk | 43fb2a8ceb | |
Filip Krawczyk | 4cd8b9855c | |
Filip Krawczyk | 1fcc95d6b9 | |
Filip Krawczyk | cd1f7a3be3 | |
Filip Krawczyk | 579b4e1d5d | |
Filip Krawczyk | 09f1f54c05 | |
Filip Krawczyk | 462ce5df76 | |
Filip Krawczyk | 63032ebae1 | |
Filip Krawczyk | 116b0d7961 | |
Filip Krawczyk | 663b45bc21 | |
Filip Krawczyk | 79f68ee732 | |
Filip Krawczyk | 34be4c13a1 | |
Filip Krawczyk | 52498a6be3 | |
Filip Krawczyk | 3c295552df | |
Filip Krawczyk | 062a53fdd9 | |
Filip Krawczyk | 33ff99510f | |
Filip Krawczyk | e91e5789ee | |
Filip Krawczyk | 81fadffa14 | |
Filip Krawczyk | 6f271ffc91 | |
Filip Krawczyk | 14b7813243 | |
Filip Krawczyk | ce41b7e18c | |
Filip Krawczyk | 85108d8965 | |
Filip Krawczyk | 08831afbcf | |
Marcin Wojnarowski | 44cd556fef | |
shilangyu | 22030d6291 | |
shilangyu | 9ed7b89d68 | |
shilangyu | 75622a735b | |
shilangyu | d113e01eeb | |
Filip Krawczyk | 3c274adee7 | |
Filip Krawczyk | 8c0c478847 | |
Filip Krawczyk | cab4aeebb7 | |
Filip Krawczyk | 7db538084a | |
Filip Krawczyk | b5bb5dc1ff | |
Filip Krawczyk | f21c6b7c8c | |
Filip Krawczyk | b972e4485a | |
Filip Krawczyk | 6f8fed149c | |
Filip Krawczyk | fa2a3be6b5 | |
shilangyu | 6136e4a408 | |
shilangyu | 9f588a5ea8 | |
shilangyu | 4533be634e | |
shilangyu | 3f33cac3ed | |
Marcin Wojnarowski | 28be50a89e | |
shilangyu | 926c0afe0a | |
shilangyu | df85b6271e | |
Marcin Wojnarowski | 476ad85a38 | |
shilangyu | ab1873a19f | |
shilangyu | 7bb8d2b33f | |
Filip Krawczyk | d2c85cb305 | |
shilangyu | c730216e13 | |
shilangyu | d0b7c0776f | |
shilangyu | 44162f282c | |
shilangyu | 23f64387a7 | |
shilangyu | d64fe48328 | |
shilangyu | 85f9d3fd0e | |
shilangyu | 8838efc073 | |
shilangyu | f6191936e6 | |
shilangyu | c320585810 | |
shilangyu | 8c54e38e99 | |
shilangyu | a821681903 | |
shilangyu | 5068eb900f | |
shilangyu | 816b7d1346 | |
shilangyu | f5b02a369b | |
shilangyu | 873ad16baf | |
shilangyu | c34f681602 | |
Marcin Wojnarowski | 11d63e0522 | |
shilangyu | 553340aa38 | |
shilangyu | cd0343e999 | |
shilangyu | bea100ed46 | |
shilangyu | 8f88ad4fe5 | |
Marcin Wojnarowski | c414033e57 | |
shilangyu | 389d1381b4 | |
shilangyu | 652e912950 | |
shilangyu | a615b27d64 | |
shilangyu | 766762078c | |
shilangyu | 9e7793f949 | |
shilangyu | 2b6ce0e6b2 | |
shilangyu | eeb9a84b6b | |
shilangyu | 7aad355b21 | |
shilangyu | 6a814ab128 | |
shilangyu | 8eb4672bcd | |
shilangyu | 7a13a94e51 | |
Marcin Wojnarowski | 88608ea9e1 | |
Filip Krawczyk | 56bba4d6af | |
Filip Krawczyk | a0b8fd4c05 | |
github-actions[bot] | 52bd797eb8 | |
Marcin Wojnarowski | 93735dfad8 |
|
@ -69,7 +69,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v1
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
|
@ -91,10 +91,6 @@ jobs:
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev
|
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev
|
||||||
|
|
||||||
- name: Enable linux support
|
|
||||||
run: |
|
|
||||||
flutter config --enable-linux-desktop
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
flutter build linux --release --target lib/main_prod.dart
|
flutter build linux --release --target lib/main_prod.dart
|
||||||
|
@ -110,10 +106,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
- name: Enable windows support
|
|
||||||
run: |
|
|
||||||
flutter config --enable-windows-desktop
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
flutter build windows --release --target lib/main_prod.dart
|
flutter build windows --release --target lib/main_prod.dart
|
||||||
|
|
|
@ -90,10 +90,6 @@ jobs:
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev
|
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev
|
||||||
|
|
||||||
- name: Enable linux support
|
|
||||||
run: |
|
|
||||||
flutter config --enable-linux-desktop
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
flutter build linux --release --target lib/main_prod.dart
|
flutter build linux --release --target lib/main_prod.dart
|
||||||
|
@ -119,10 +115,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
- name: Enable windows support
|
|
||||||
run: |
|
|
||||||
flutter config --enable-windows-desktop
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
flutter build windows --release --target lib/main_prod.dart
|
flutter build windows --release --target lib/main_prod.dart
|
||||||
|
@ -165,7 +157,6 @@ jobs:
|
||||||
linux-build/*
|
linux-build/*
|
||||||
windows-build/*
|
windows-build/*
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: true
|
|
||||||
body_path: current-changelog.txt
|
body_path: current-changelog.txt
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
name: weblate
|
name: weblate
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# every friday at 19:00 UTC
|
# every friday at 19:00 UTC
|
||||||
- cron: "0 19 * * 5"
|
- cron: "0 19 * * 5"
|
||||||
|
@ -11,13 +12,19 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Fetch changes
|
- name: Fetch changes
|
||||||
run: |
|
run: |
|
||||||
git remote add weblate http://weblate.yerbamate.ml/git/lemmur/lemmur
|
git remote add weblate https://weblate.yerbamate.ml/git/lemmur/lemmur/
|
||||||
git fetch weblate
|
git fetch weblate
|
||||||
git merge weblate/master
|
git merge weblate/master
|
||||||
|
|
||||||
|
- name: Regenerate l10n_from_string
|
||||||
|
run: |
|
||||||
|
dart run scripts/gen_l10n_from_string.dart
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v3.12.0
|
uses: peter-evans/create-pull-request@v3.12.0
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"L10n string": {
|
"L10n string": {
|
||||||
"scope": "dart",
|
"scope": "dart",
|
||||||
"prefix": "l10n",
|
"prefix": "l10n",
|
||||||
"body": ["L10n.of(context)!.$0"]
|
"body": ["L10n.of(context).$0"]
|
||||||
},
|
},
|
||||||
"Mobx store": {
|
"Mobx store": {
|
||||||
"prefix": "mobxstore",
|
"prefix": "mobxstore",
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Create post community picker now has autocomplete
|
||||||
|
- You can now add an instance from the three dots menu on the instance page
|
||||||
|
- Editor for writing comments, posts, and profile bio now has a toolbar with shortcuts to all the common markdown things
|
||||||
|
|
||||||
## v0.8.0 - 2022-01-14
|
## v0.8.0 - 2022-01-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Privacy Policy
|
||||||
|
|
||||||
|
We don't store your data. We don't use any intermediary services that could store your data.
|
||||||
|
|
||||||
|
For any questions contact us at lemmurapp@protonmail.com
|
19
README.md
19
README.md
|
@ -1,6 +1,13 @@
|
||||||
|
# ⚠️ THIS PROJECT IS NOT MAINTAINED ANYMORE ⚠️
|
||||||
|
|
||||||
|
This project has been officially dropped due to lack of interest and political differences. If anyone is interested in continuing developement, feel free to fork it. For any questions you can message [krawieck](https://matrix.to/#/@krawieck:matrix.org) (who was responsible for the flutter app) or [shilangyu](https://matrix.to/#/@shilangyu:matrix.org) (who was responsible for lemmy_api_client).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[![](https://github.com/LemmurOrg/lemmur/workflows/ci/badge.svg)](https://github.com/LemmurOrg/lemmur/actions)
|
[![](https://github.com/LemmurOrg/lemmur/workflows/ci/badge.svg)](https://github.com/LemmurOrg/lemmur/actions)
|
||||||
|
[![Translation status](http://weblate.yerbamate.ml/widgets/lemmur/-/lemmur/svg-badge.svg)](http://weblate.yerbamate.ml/engage/lemmur/)
|
||||||
|
|
||||||
<img width=200px height=200px src="https://raw.githubusercontent.com/LemmurOrg/lemmur/master/assets/readme_icon.svg"/>
|
<img width=200px height=200px src="https://raw.githubusercontent.com/LemmurOrg/lemmur/master/assets/readme_icon.svg"/>
|
||||||
|
|
||||||
|
@ -43,22 +50,14 @@ The apk will be in `build/app/outputs/flutter-apk/app-prod-release.apk`
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
1. Make sure you have the additional [linux requirements](https://flutter.dev/desktop#additional-linux-requirements) (verify with `flutter doctor`)
|
1. Make sure you have the additional [linux requirements](https://flutter.dev/desktop#additional-linux-requirements) (verify with `flutter doctor`)
|
||||||
2. Enable linux desktop:
|
2. Build: `flutter build linux --target lib/main_prod.dart --release`
|
||||||
```sh
|
|
||||||
flutter config --enable-linux-desktop
|
|
||||||
```
|
|
||||||
3. Build: `flutter build linux --target lib/main_prod.dart --release`
|
|
||||||
|
|
||||||
The executable will be in `build/linux/x64/release/bundle/lemmur` (be aware, however, that this executable is not standalone)
|
The executable will be in `build/linux/x64/release/bundle/lemmur` (be aware, however, that this executable is not standalone)
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
1. Make sure you have the additional [windows requirements](https://flutter.dev/desktop#additional-windows-requirements) (verify with `flutter doctor`)
|
1. Make sure you have the additional [windows requirements](https://flutter.dev/desktop#additional-windows-requirements) (verify with `flutter doctor`)
|
||||||
2. Enable windows desktop:
|
2. Build: `flutter build windows --target lib/main_prod.dart --release`
|
||||||
```sh
|
|
||||||
flutter config --enable-windows-desktop
|
|
||||||
```
|
|
||||||
3. Build: `flutter build windows --target lib/main_prod.dart --release`
|
|
||||||
|
|
||||||
The executable will be in `build\windows\runner\Release\lemmur.exe` (be aware, however, that this executable is not standalone)
|
The executable will be in `build\windows\runner\Release\lemmur.exe` (be aware, however, that this executable is not standalone)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ linter:
|
||||||
- avoid_catching_errors
|
- avoid_catching_errors
|
||||||
- avoid_equals_and_hash_code_on_mutable_classes
|
- avoid_equals_and_hash_code_on_mutable_classes
|
||||||
- avoid_escaping_inner_quotes
|
- avoid_escaping_inner_quotes
|
||||||
|
- avoid_final_parameters
|
||||||
- avoid_function_literals_in_foreach_calls
|
- avoid_function_literals_in_foreach_calls
|
||||||
- avoid_init_to_null
|
- avoid_init_to_null
|
||||||
- avoid_null_checks_in_equality_operators
|
- avoid_null_checks_in_equality_operators
|
||||||
|
@ -29,6 +30,7 @@ linter:
|
||||||
- cascade_invocations
|
- cascade_invocations
|
||||||
- cast_nullable_to_non_nullable
|
- cast_nullable_to_non_nullable
|
||||||
- constant_identifier_names
|
- constant_identifier_names
|
||||||
|
- conditional_uri_does_not_exist
|
||||||
- curly_braces_in_flow_control_structures
|
- curly_braces_in_flow_control_structures
|
||||||
- directives_ordering
|
- directives_ordering
|
||||||
- empty_catches
|
- empty_catches
|
||||||
|
@ -41,8 +43,11 @@ linter:
|
||||||
- invariant_booleans
|
- invariant_booleans
|
||||||
- library_names
|
- library_names
|
||||||
- library_prefixes
|
- library_prefixes
|
||||||
|
- literal_only_boolean_expressions
|
||||||
- non_constant_identifier_names
|
- non_constant_identifier_names
|
||||||
- noop_primitive_operations
|
- noop_primitive_operations
|
||||||
|
- no_leading_underscores_for_library_prefixes
|
||||||
|
- no_leading_underscores_for_local_identifiers
|
||||||
- null_check_on_nullable_type_parameter
|
- null_check_on_nullable_type_parameter
|
||||||
- omit_local_variable_types
|
- omit_local_variable_types
|
||||||
- one_member_abstracts
|
- one_member_abstracts
|
||||||
|
@ -81,7 +86,9 @@ linter:
|
||||||
- prefer_spread_collections
|
- prefer_spread_collections
|
||||||
- prefer_typing_uninitialized_variables
|
- prefer_typing_uninitialized_variables
|
||||||
- recursive_getters
|
- recursive_getters
|
||||||
|
- secure_pubspec_urls
|
||||||
- sized_box_for_whitespace
|
- sized_box_for_whitespace
|
||||||
|
- sized_box_shrink_expand
|
||||||
- slash_for_doc_comments
|
- slash_for_doc_comments
|
||||||
- sort_child_properties_last
|
- sort_child_properties_last
|
||||||
- sort_unnamed_constructors_first
|
- sort_unnamed_constructors_first
|
||||||
|
@ -94,6 +101,7 @@ linter:
|
||||||
- unnecessary_constructor_name
|
- unnecessary_constructor_name
|
||||||
- unnecessary_getters_setters
|
- unnecessary_getters_setters
|
||||||
- unnecessary_lambdas
|
- unnecessary_lambdas
|
||||||
|
- unnecessary_late
|
||||||
- unnecessary_new
|
- unnecessary_new
|
||||||
- unnecessary_null_aware_assignments
|
- unnecessary_null_aware_assignments
|
||||||
- unnecessary_null_checks
|
- unnecessary_null_checks
|
||||||
|
@ -104,12 +112,15 @@ linter:
|
||||||
- unnecessary_string_interpolations
|
- unnecessary_string_interpolations
|
||||||
- unnecessary_this
|
- unnecessary_this
|
||||||
- unrelated_type_equality_checks
|
- unrelated_type_equality_checks
|
||||||
|
- use_colored_box
|
||||||
|
- use_enums
|
||||||
- use_full_hex_values_for_flutter_colors
|
- use_full_hex_values_for_flutter_colors
|
||||||
- use_is_even_rather_than_modulo
|
- use_is_even_rather_than_modulo
|
||||||
- use_named_constants
|
- use_named_constants
|
||||||
- use_raw_strings
|
- use_raw_strings
|
||||||
- use_rethrow_when_possible
|
- use_rethrow_when_possible
|
||||||
- use_setters_to_change_properties
|
- use_setters_to_change_properties
|
||||||
|
- use_super_parameters
|
||||||
- use_test_throws_matchers
|
- use_test_throws_matchers
|
||||||
- use_to_and_as_if_applicable
|
- use_to_and_as_if_applicable
|
||||||
- void_checks
|
- void_checks
|
||||||
|
|
|
@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion Math.max(flutter.compileSdkVersion, 32)
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
|
@ -47,8 +47,8 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.krawieck.lemmur"
|
applicationId "com.krawieck.lemmur"
|
||||||
minSdkVersion 16
|
minSdkVersion Math.max(flutter.minSdkVersion, 16)
|
||||||
targetSdkVersion 30
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,13 @@
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name="${applicationName}"
|
||||||
android:label="${appName}"
|
android:label="${appName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.50'
|
ext.kotlin_version = '1.7.0'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
|
||||||
|
|
|
@ -215,7 +215,10 @@
|
||||||
"number_of_users_online": "{formattedCount,plural, =1{{formattedCount} user online} other{{formattedCount} users online}}",
|
"number_of_users_online": "{formattedCount,plural, =1{{formattedCount} user online} other{{formattedCount} users online}}",
|
||||||
"@number_of_users_online": {
|
"@number_of_users_online": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"formattedCount": {}
|
"formattedCount": {
|
||||||
|
"type": "int",
|
||||||
|
"format": "compact"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"number_of_comments": "{formattedCount,plural, =1{{formattedCount} comment} other{{formattedCount} comments}}",
|
"number_of_comments": "{formattedCount,plural, =1{{formattedCount} comment} other{{formattedCount} comments}}",
|
||||||
|
@ -239,13 +242,28 @@
|
||||||
"number_of_subscribers": "{formattedCount,plural, =1{{formattedCount} subscriber} other{{formattedCount} subscribers}}",
|
"number_of_subscribers": "{formattedCount,plural, =1{{formattedCount} subscriber} other{{formattedCount} subscribers}}",
|
||||||
"@number_of_subscribers": {
|
"@number_of_subscribers": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"formattedCount": {}
|
"formattedCount": {
|
||||||
|
"type": "int",
|
||||||
|
"format": "compact"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"number_of_users": "{formattedCount,plural, =1{{formattedCount} user} other{{formattedCount} users}}",
|
"number_of_users": "{formattedCount,plural, =1{{formattedCount} user} other{{formattedCount} users}}",
|
||||||
"@number_of_users": {
|
"@number_of_users": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"formattedCount": {}
|
"formattedCount": {
|
||||||
|
"type": "int",
|
||||||
|
"format": "compact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number_of_communities": "{formattedCount,plural, =1{{formattedCount} community} other{{formattedCount} communities}}",
|
||||||
|
"@number_of_communities": {
|
||||||
|
"placeholders": {
|
||||||
|
"formattedCount": {
|
||||||
|
"type": "int",
|
||||||
|
"format": "compact"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unsubscribe": "unsubscribe",
|
"unsubscribe": "unsubscribe",
|
||||||
|
@ -291,5 +309,135 @@
|
||||||
"show_bot_accounts": "Show Bot Accounts",
|
"show_bot_accounts": "Show Bot Accounts",
|
||||||
"@show_bot_accounts": {},
|
"@show_bot_accounts": {},
|
||||||
"show_read_posts": "Show Read Posts",
|
"show_read_posts": "Show Read Posts",
|
||||||
"@show_read_posts": {}
|
"@show_read_posts": {},
|
||||||
|
"site_not_set_up": "This site has not yet been set up",
|
||||||
|
"@site_not_set_up": {},
|
||||||
|
"nerd_stuff": "Nerd stuff",
|
||||||
|
"@nerd_stuff": {},
|
||||||
|
"open_in_browser": "Open in browser",
|
||||||
|
"@open_in_browser": {},
|
||||||
|
"cannot_open_in_browser": "Can't open in browser",
|
||||||
|
"@cannot_open_in_browser": {},
|
||||||
|
"about": "About",
|
||||||
|
"@about": {},
|
||||||
|
"see_all": "See all",
|
||||||
|
"@see_all": {},
|
||||||
|
"admins": "Admins",
|
||||||
|
"@admins": {},
|
||||||
|
"trending_communities": "Trending communities",
|
||||||
|
"@trending_communities": {},
|
||||||
|
"communities_of_instance": "Communities of {instance}",
|
||||||
|
"@communities_of_instance": {
|
||||||
|
"placeholders": {
|
||||||
|
"instance": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"day": "day",
|
||||||
|
"@day": {},
|
||||||
|
"week": "week",
|
||||||
|
"@week": {},
|
||||||
|
"month": "month",
|
||||||
|
"@month": {},
|
||||||
|
"six_months": "6 months",
|
||||||
|
"@six_months": {},
|
||||||
|
"add_instance": "Add instance",
|
||||||
|
"@add_instance": {},
|
||||||
|
"instance_added": "Instance successfully added",
|
||||||
|
"@instance_added": {},
|
||||||
|
"required_field": "required field",
|
||||||
|
"@required_field": {},
|
||||||
|
"no_communities_found": "No communities found",
|
||||||
|
"@no_communities_found": {},
|
||||||
|
"network_error": "Network error",
|
||||||
|
"@network_error": {},
|
||||||
|
"editor_bold": "bold",
|
||||||
|
"@editor_bold": {
|
||||||
|
"description": "tooltip for button making text bold in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_italics": "italics",
|
||||||
|
"@editor_italics": {
|
||||||
|
"description": "tooltip for button making text italics in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_link": "insert link",
|
||||||
|
"@editor_link": {
|
||||||
|
"description": "tooltip for button that inserts link in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_image": "insert image",
|
||||||
|
"@editor_image": {
|
||||||
|
"description": "tooltip for button that inserts image in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_user": "link user",
|
||||||
|
"@editor_user": {
|
||||||
|
"description": "tooltip for button that opens a popup to select user to be linked in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_community": "link community",
|
||||||
|
"@editor_community": {
|
||||||
|
"description": "tooltip for button that opens a popup to select community to be linked in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_header": "insert header",
|
||||||
|
"@editor_header": {
|
||||||
|
"description": "tooltip for button that inserts header in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_strikethrough": "strikethrough",
|
||||||
|
"@editor_strikethrough": {
|
||||||
|
"description": "tooltip for button that makes text strikethrough in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_quote": "quote",
|
||||||
|
"@editor_quote": {
|
||||||
|
"description": "tooltip for button that makes selected text into quote blocks in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_list": "list",
|
||||||
|
"@editor_list": {
|
||||||
|
"description": "tooltip for button that makes selected text into list in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_code": "code",
|
||||||
|
"@editor_code": {
|
||||||
|
"description": "tooltip for button that makes text into code in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_subscript": "subscript",
|
||||||
|
"@editor_subscript": {
|
||||||
|
"description": "tooltip for button that makes text into subscript in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_superscript": "superscript",
|
||||||
|
"@editor_superscript": {
|
||||||
|
"description": "tooltip for button that makes text into superscript in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_spoiler": "spoiler",
|
||||||
|
"@editor_spoiler": {
|
||||||
|
"description": "tooltip for button that inserts spoiler in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_help": "markdown guide",
|
||||||
|
"@editor_help": {
|
||||||
|
"description": "tooltip for button that goes to page containing a guide for markdown"
|
||||||
|
},
|
||||||
|
"insert_text_here_placeholder": "[write text here]",
|
||||||
|
"@insert_text_here_placeholder": {
|
||||||
|
"description": "placeholder for text in markdown editor when inserting stuff like * for italics or ** for bold, etc."
|
||||||
|
},
|
||||||
|
"select_user": "Select User",
|
||||||
|
"@select_user": {
|
||||||
|
"description": "Title on a popup that lets a user search and select another user"
|
||||||
|
},
|
||||||
|
"select_community": "Select Community",
|
||||||
|
"@select_community": {
|
||||||
|
"description": "Title on a popup that lets a user search and select a community"
|
||||||
|
},
|
||||||
|
"add_link": "Add link",
|
||||||
|
"@add_link": {
|
||||||
|
"description": "title on top of a link insertion popup in a markdown editor"
|
||||||
|
},
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"@cancel": {
|
||||||
|
"description": "Cancel button on popup"
|
||||||
|
},
|
||||||
|
"editor_add_link_label": "label",
|
||||||
|
"@editor_add_link_label": {
|
||||||
|
"description": "palceholder for link label on an Add link popup in markdown editor"
|
||||||
|
},
|
||||||
|
"failed_to_upload_image": "Failed to upload image",
|
||||||
|
"@failed_to_upload_image": {
|
||||||
|
"description": "shows up on a snackbar when the image upload failed (duh)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,132 +1,269 @@
|
||||||
{
|
{
|
||||||
"@@locale": "ja",
|
"@@locale": "ja",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
|
"@settings": {},
|
||||||
"password": "パスワード",
|
"password": "パスワード",
|
||||||
|
"@password": {},
|
||||||
"email_or_username": "メールアドレスまたはユーザー名",
|
"email_or_username": "メールアドレスまたはユーザー名",
|
||||||
|
"@email_or_username": {},
|
||||||
"posts": "投稿",
|
"posts": "投稿",
|
||||||
|
"@posts": {},
|
||||||
"comments": "コメント",
|
"comments": "コメント",
|
||||||
|
"@comments": {},
|
||||||
"modlog": "モデレーションログ",
|
"modlog": "モデレーションログ",
|
||||||
|
"@modlog": {},
|
||||||
"community": "コミュニティ",
|
"community": "コミュニティ",
|
||||||
|
"@community": {},
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"@url": {},
|
||||||
"title": "タイトル",
|
"title": "タイトル",
|
||||||
|
"@title": {},
|
||||||
"body": "本文",
|
"body": "本文",
|
||||||
|
"@body": {},
|
||||||
"nsfw": "閲覧注意",
|
"nsfw": "閲覧注意",
|
||||||
|
"@nsfw": {},
|
||||||
"post": "投稿",
|
"post": "投稿",
|
||||||
|
"@post": {},
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
|
"@save": {},
|
||||||
"subscribed": "登録済み",
|
"subscribed": "登録済み",
|
||||||
|
"@subscribed": {},
|
||||||
"local": "インスタンス内",
|
"local": "インスタンス内",
|
||||||
|
"@local": {},
|
||||||
"all": "全て",
|
"all": "全て",
|
||||||
|
"@all": {},
|
||||||
"replies": "返信",
|
"replies": "返信",
|
||||||
|
"@replies": {},
|
||||||
"mentions": "言及",
|
"mentions": "言及",
|
||||||
|
"@mentions": {},
|
||||||
"from": "から",
|
"from": "から",
|
||||||
|
"@from": {},
|
||||||
"to": "投稿先",
|
"to": "投稿先",
|
||||||
|
"@to": {},
|
||||||
"deleted_by_creator": "投稿者によって削除済み",
|
"deleted_by_creator": "投稿者によって削除済み",
|
||||||
|
"@deleted_by_creator": {},
|
||||||
"more": "さらに表示",
|
"more": "さらに表示",
|
||||||
|
"@more": {},
|
||||||
"mark_as_read": "既読にする",
|
"mark_as_read": "既読にする",
|
||||||
|
"@mark_as_read": {},
|
||||||
"mark_as_unread": "未読にする",
|
"mark_as_unread": "未読にする",
|
||||||
|
"@mark_as_unread": {},
|
||||||
"reply": "返信",
|
"reply": "返信",
|
||||||
|
"@reply": {},
|
||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
|
"@edit": {},
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
|
"@delete": {},
|
||||||
"restore": "復元",
|
"restore": "復元",
|
||||||
|
"@restore": {},
|
||||||
"yes": "はい",
|
"yes": "はい",
|
||||||
|
"@yes": {},
|
||||||
"no": "いいえ",
|
"no": "いいえ",
|
||||||
|
"@no": {},
|
||||||
"avatar": "アバター",
|
"avatar": "アバター",
|
||||||
|
"@avatar": {},
|
||||||
"banner": "バナー",
|
"banner": "バナー",
|
||||||
"display_name": "アカウント名",
|
"@banner": {},
|
||||||
|
"display_name": "表示名",
|
||||||
|
"@display_name": {},
|
||||||
"bio": "自己紹介",
|
"bio": "自己紹介",
|
||||||
|
"@bio": {},
|
||||||
"email": "メールアドレス",
|
"email": "メールアドレス",
|
||||||
|
"@email": {},
|
||||||
"matrix_user": "Matrix のユーザーアカウント",
|
"matrix_user": "Matrix のユーザーアカウント",
|
||||||
|
"@matrix_user": {},
|
||||||
"sort_type": "投稿やコメントの並び順",
|
"sort_type": "投稿やコメントの並び順",
|
||||||
"type": "コミュニティの表示",
|
"@sort_type": {},
|
||||||
|
"type": "検索対象",
|
||||||
|
"@type": {},
|
||||||
"show_nsfw": "閲覧注意のコンテンツを表示",
|
"show_nsfw": "閲覧注意のコンテンツを表示",
|
||||||
|
"@show_nsfw": {},
|
||||||
"send_notifications_to_email": "通知を設定したメールアドレスに送信",
|
"send_notifications_to_email": "通知を設定したメールアドレスに送信",
|
||||||
|
"@send_notifications_to_email": {},
|
||||||
"delete_account": "アカウントを削除",
|
"delete_account": "アカウントを削除",
|
||||||
|
"@delete_account": {},
|
||||||
"saved": "保存済み",
|
"saved": "保存済み",
|
||||||
|
"@saved": {},
|
||||||
"communities": "コミュニティ",
|
"communities": "コミュニティ",
|
||||||
|
"@communities": {},
|
||||||
"users": "ユーザー",
|
"users": "ユーザー",
|
||||||
|
"@users": {},
|
||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
|
"@theme": {},
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
|
"@language": {},
|
||||||
"hot": "人気",
|
"hot": "人気",
|
||||||
|
"@hot": {},
|
||||||
"new_": "新しい順",
|
"new_": "新しい順",
|
||||||
|
"@new_": {},
|
||||||
"old": "古い順",
|
"old": "古い順",
|
||||||
|
"@old": {},
|
||||||
"top": "トップ",
|
"top": "トップ",
|
||||||
|
"@top": {},
|
||||||
"chat": "会話",
|
"chat": "会話",
|
||||||
|
"@chat": {},
|
||||||
"admin": "管理者",
|
"admin": "管理者",
|
||||||
|
"@admin": {},
|
||||||
"by": "投稿者",
|
"by": "投稿者",
|
||||||
|
"@by": {},
|
||||||
"not_a_mod_or_admin": "モデレーターまたは管理者ではありません。",
|
"not_a_mod_or_admin": "モデレーターまたは管理者ではありません。",
|
||||||
|
"@not_a_mod_or_admin": {},
|
||||||
"not_an_admin": "管理者ではありません。",
|
"not_an_admin": "管理者ではありません。",
|
||||||
|
"@not_an_admin": {},
|
||||||
"couldnt_find_post": "投稿が見つかりせんでした。",
|
"couldnt_find_post": "投稿が見つかりせんでした。",
|
||||||
|
"@couldnt_find_post": {},
|
||||||
"not_logged_in": "ログインしていません。",
|
"not_logged_in": "ログインしていません。",
|
||||||
|
"@not_logged_in": {},
|
||||||
"site_ban": "サイトへのアクセスを禁止されています",
|
"site_ban": "サイトへのアクセスを禁止されています",
|
||||||
|
"@site_ban": {},
|
||||||
"community_ban": "このコミュニティへのアクセスを禁止されています。",
|
"community_ban": "このコミュニティへのアクセスを禁止されています。",
|
||||||
|
"@community_ban": {},
|
||||||
"downvotes_disabled": "反対票を無効化",
|
"downvotes_disabled": "反対票を無効化",
|
||||||
|
"@downvotes_disabled": {},
|
||||||
"invalid_url": "無効なURL。",
|
"invalid_url": "無効なURL。",
|
||||||
|
"@invalid_url": {},
|
||||||
"locked": "凍結中",
|
"locked": "凍結中",
|
||||||
|
"@locked": {},
|
||||||
"couldnt_create_comment": "投稿が作成されませんでした。",
|
"couldnt_create_comment": "投稿が作成されませんでした。",
|
||||||
|
"@couldnt_create_comment": {},
|
||||||
"couldnt_like_comment": "コメントを「いいね」できませんでした。",
|
"couldnt_like_comment": "コメントを「いいね」できませんでした。",
|
||||||
|
"@couldnt_like_comment": {},
|
||||||
"couldnt_update_comment": "コメントが更新されませんでした。",
|
"couldnt_update_comment": "コメントが更新されませんでした。",
|
||||||
|
"@couldnt_update_comment": {},
|
||||||
"no_comment_edit_allowed": "コメントの編集権限がありません。",
|
"no_comment_edit_allowed": "コメントの編集権限がありません。",
|
||||||
|
"@no_comment_edit_allowed": {},
|
||||||
"couldnt_save_comment": "コメントが保存されませんでした。",
|
"couldnt_save_comment": "コメントが保存されませんでした。",
|
||||||
|
"@couldnt_save_comment": {},
|
||||||
"couldnt_get_comments": "コメントを読み込みできませんでした。",
|
"couldnt_get_comments": "コメントを読み込みできませんでした。",
|
||||||
|
"@couldnt_get_comments": {},
|
||||||
"invalid_post_title": "無効な投稿のタイトル",
|
"invalid_post_title": "無効な投稿のタイトル",
|
||||||
|
"@invalid_post_title": {},
|
||||||
"couldnt_create_post": "投稿が作成されませんでした。",
|
"couldnt_create_post": "投稿が作成されませんでした。",
|
||||||
|
"@couldnt_create_post": {},
|
||||||
"couldnt_like_post": "投稿を「いいね」できませんでした。",
|
"couldnt_like_post": "投稿を「いいね」できませんでした。",
|
||||||
|
"@couldnt_like_post": {},
|
||||||
"couldnt_find_community": "コミュニティが見つかりませんでした。",
|
"couldnt_find_community": "コミュニティが見つかりませんでした。",
|
||||||
|
"@couldnt_find_community": {},
|
||||||
"couldnt_get_posts": "投稿を読み込みできませんでした",
|
"couldnt_get_posts": "投稿を読み込みできませんでした",
|
||||||
|
"@couldnt_get_posts": {},
|
||||||
"no_post_edit_allowed": "投稿の編集権限がありません。",
|
"no_post_edit_allowed": "投稿の編集権限がありません。",
|
||||||
|
"@no_post_edit_allowed": {},
|
||||||
"couldnt_save_post": "投稿が保存されませんでした。",
|
"couldnt_save_post": "投稿が保存されませんでした。",
|
||||||
|
"@couldnt_save_post": {},
|
||||||
"site_already_exists": "サイトは既に存在します。",
|
"site_already_exists": "サイトは既に存在します。",
|
||||||
|
"@site_already_exists": {},
|
||||||
"couldnt_update_site": "サイトが更新されませんでした。",
|
"couldnt_update_site": "サイトが更新されませんでした。",
|
||||||
"invalid_community_name": "無効な名前です。",
|
"@couldnt_update_site": {},
|
||||||
|
"invalid_community_name": "無効なコミュニティの名前です。",
|
||||||
|
"@invalid_community_name": {},
|
||||||
"community_already_exists": "コミュニティは既に存在します。",
|
"community_already_exists": "コミュニティは既に存在します。",
|
||||||
|
"@community_already_exists": {},
|
||||||
"community_moderator_already_exists": "コミュニティのモデレーターは既に存在しています。",
|
"community_moderator_already_exists": "コミュニティのモデレーターは既に存在しています。",
|
||||||
|
"@community_moderator_already_exists": {},
|
||||||
"community_follower_already_exists": "コミュニティへのフォロワーは既に存在します。",
|
"community_follower_already_exists": "コミュニティへのフォロワーは既に存在します。",
|
||||||
|
"@community_follower_already_exists": {},
|
||||||
"not_a_moderator": "モデレーターではありません。",
|
"not_a_moderator": "モデレーターではありません。",
|
||||||
|
"@not_a_moderator": {},
|
||||||
"couldnt_update_community": "コミュニティを更新できませんでした。",
|
"couldnt_update_community": "コミュニティを更新できませんでした。",
|
||||||
|
"@couldnt_update_community": {},
|
||||||
"no_community_edit_allowed": "コミュニティの編集許可がありません。",
|
"no_community_edit_allowed": "コミュニティの編集許可がありません。",
|
||||||
"system_err_login": "システムエラーが発生しました。一度ログアウトして、再度ログインをお試しください。",
|
"@no_community_edit_allowed": {},
|
||||||
|
"system_err_login": "システムエラーが発生しました。一度ログアウトして再度ログインしてから、お試しください。",
|
||||||
|
"@system_err_login": {},
|
||||||
"community_user_already_banned": "コミュニティのユーザーは既にアクセスが禁止されています。",
|
"community_user_already_banned": "コミュニティのユーザーは既にアクセスが禁止されています。",
|
||||||
|
"@community_user_already_banned": {},
|
||||||
"couldnt_find_that_username_or_email": "ユーザー名またはメールアドレスが見つかりませんでした。",
|
"couldnt_find_that_username_or_email": "ユーザー名またはメールアドレスが見つかりませんでした。",
|
||||||
|
"@couldnt_find_that_username_or_email": {},
|
||||||
"password_incorrect": "無効なパスワードです。",
|
"password_incorrect": "無効なパスワードです。",
|
||||||
|
"@password_incorrect": {},
|
||||||
"registration_closed": "新規登録は受け付けていません",
|
"registration_closed": "新規登録は受け付けていません",
|
||||||
|
"@registration_closed": {},
|
||||||
"invalid_password": "無効なパスワードです。パスワードは必ず60文字以下にしてください。",
|
"invalid_password": "無効なパスワードです。パスワードは必ず60文字以下にしてください。",
|
||||||
|
"@invalid_password": {},
|
||||||
"passwords_dont_match": "パスワードが一致しません。",
|
"passwords_dont_match": "パスワードが一致しません。",
|
||||||
|
"@passwords_dont_match": {},
|
||||||
"captcha_incorrect": "Captchaコードが合っていません。",
|
"captcha_incorrect": "Captchaコードが合っていません。",
|
||||||
"invalid_username": "無効なユーザーネームです。",
|
"@captcha_incorrect": {},
|
||||||
|
"invalid_username": "無効なユーザー名です。",
|
||||||
|
"@invalid_username": {},
|
||||||
"bio_length_overflow": "自己紹介は 300 文字までです。",
|
"bio_length_overflow": "自己紹介は 300 文字までです。",
|
||||||
"couldnt_update_user": "ユーザーが更新されない。",
|
"@bio_length_overflow": {},
|
||||||
|
"couldnt_update_user": "ユーザーが更新されませんでした。",
|
||||||
|
"@couldnt_update_user": {},
|
||||||
"couldnt_update_private_message": "プライベートメッセージが更新されませんでした。",
|
"couldnt_update_private_message": "プライベートメッセージが更新されませんでした。",
|
||||||
|
"@couldnt_update_private_message": {},
|
||||||
"couldnt_update_post": "投稿が更新されませんでした",
|
"couldnt_update_post": "投稿が更新されませんでした",
|
||||||
|
"@couldnt_update_post": {},
|
||||||
"couldnt_create_private_message": "プライベートメッセージが作成されませんでした。",
|
"couldnt_create_private_message": "プライベートメッセージが作成されませんでした。",
|
||||||
|
"@couldnt_create_private_message": {},
|
||||||
"no_private_message_edit_allowed": "プライベートメッセージの編集許可がありません。",
|
"no_private_message_edit_allowed": "プライベートメッセージの編集許可がありません。",
|
||||||
|
"@no_private_message_edit_allowed": {},
|
||||||
"post_title_too_long": "投稿のタイトルが長すぎます。",
|
"post_title_too_long": "投稿のタイトルが長すぎます。",
|
||||||
|
"@post_title_too_long": {},
|
||||||
"email_already_exists": "メールアドレスが既に使用されています。",
|
"email_already_exists": "メールアドレスが既に使用されています。",
|
||||||
|
"@email_already_exists": {},
|
||||||
"user_already_exists": "ユーザーは既に存在します。",
|
"user_already_exists": "ユーザーは既に存在します。",
|
||||||
"number_of_users_online": "{formattedCount,plural, other{{formattedCount}名のユーザーがオンライン}}",
|
"@user_already_exists": {},
|
||||||
"number_of_comments": "{formattedCount,plural, other{{formattedCount}件のコメント}}",
|
"number_of_users_online": "{formattedCount,plural, =1{{formattedCount} 名のユーザーがオンライン} other{{formattedCount} 名のユーザーがオンライン}}",
|
||||||
"number_of_posts": "{formattedCount,plural, other{{formattedCount}件の投稿}}",
|
"@number_of_users_online": {},
|
||||||
"number_of_subscribers": "{formattedCount,plural, other{{formattedCount}名の登録者}}",
|
"number_of_comments": "{formattedCount,plural, =1{{formattedCount} 件のコメント} other{{formattedCount} 件のコメント}}",
|
||||||
"number_of_users": "{formattedCount,plural, other{{formattedCount}名のユーザー}}",
|
"@number_of_comments": {},
|
||||||
|
"number_of_posts": "{formattedCount,plural, =1{{formattedCount} 件の投稿} other{{formattedCount} 件の投稿}}",
|
||||||
|
"@number_of_posts": {},
|
||||||
|
"number_of_subscribers": "{formattedCount,plural, =1{{formattedCount} 名の登録者} other{{formattedCount} 名の登録者}}",
|
||||||
|
"@number_of_subscribers": {},
|
||||||
|
"number_of_users": "{formattedCount,plural, =1{{formattedCount} 名のユーザー} other{{formattedCount} 名のユーザー}}",
|
||||||
|
"@number_of_users": {},
|
||||||
"unsubscribe": "登録解除",
|
"unsubscribe": "登録解除",
|
||||||
|
"@unsubscribe": {},
|
||||||
"subscribe": "登録",
|
"subscribe": "登録",
|
||||||
|
"@subscribe": {},
|
||||||
"messages": "メッセージ",
|
"messages": "メッセージ",
|
||||||
|
"@messages": {},
|
||||||
"banned_users": "アクセス禁止してるユーザー",
|
"banned_users": "アクセス禁止してるユーザー",
|
||||||
"delete_account_confirm": "警告: あなたの全てのデータをこのインスタンスから永久に削除します。連合機能によって他のインスタンスにあるあなたのデータは、削除されないかもしれません。確認のため、パスワードを入力してください。",
|
"@banned_users": {},
|
||||||
|
"delete_account_confirm": "警告: あなたの全てのデータは、このインスタンスから永久に削除されます。しかし、Lemmyの連合機能によって他のインスタンスにある、あなたの一部のデータは削除されないかもしれません。確認のため、パスワードを入力してください。",
|
||||||
|
"@delete_account_confirm": {},
|
||||||
"new_password": "新しいパスワード",
|
"new_password": "新しいパスワード",
|
||||||
|
"@new_password": {},
|
||||||
"verify_password": "パスワードの確認",
|
"verify_password": "パスワードの確認",
|
||||||
|
"@verify_password": {},
|
||||||
"old_password": "現在のパスワード",
|
"old_password": "現在のパスワード",
|
||||||
|
"@old_password": {},
|
||||||
"show_avatars": "アバターを表示",
|
"show_avatars": "アバターを表示",
|
||||||
|
"@show_avatars": {},
|
||||||
"search": "検索",
|
"search": "検索",
|
||||||
|
"@search": {},
|
||||||
"send_message": "メッセージを送信",
|
"send_message": "メッセージを送信",
|
||||||
|
"@send_message": {},
|
||||||
"top_day": "今日の人気順",
|
"top_day": "今日の人気順",
|
||||||
|
"@top_day": {},
|
||||||
"top_week": "週間での人気順",
|
"top_week": "週間での人気順",
|
||||||
|
"@top_week": {},
|
||||||
"top_month": "月間での人気順",
|
"top_month": "月間での人気順",
|
||||||
|
"@top_month": {},
|
||||||
"top_year": "年間での人気順",
|
"top_year": "年間での人気順",
|
||||||
|
"@top_year": {},
|
||||||
"top_all": "全ての期間での人気順",
|
"top_all": "全ての期間での人気順",
|
||||||
|
"@top_all": {},
|
||||||
"most_comments": "コメントの多い順",
|
"most_comments": "コメントの多い順",
|
||||||
|
"@most_comments": {},
|
||||||
"new_comments": "新しくコメントが入った順",
|
"new_comments": "新しくコメントが入った順",
|
||||||
|
"@new_comments": {},
|
||||||
"active": "活発",
|
"active": "活発",
|
||||||
|
"@active": {},
|
||||||
"bot_account": "このアカウントをbotにする",
|
"bot_account": "このアカウントをbotにする",
|
||||||
"show_bot_accounts": "ボットアカウントを表示",
|
"@bot_account": {},
|
||||||
"show_read_posts": "読んだ投稿を表示"
|
"show_bot_accounts": "botアカウントを表示",
|
||||||
|
"@show_bot_accounts": {},
|
||||||
|
"show_read_posts": "読んだ投稿を表示",
|
||||||
|
"@show_read_posts": {},
|
||||||
|
"report_reason_required": "通報理由は必須です。",
|
||||||
|
"@report_reason_required": {},
|
||||||
|
"report_too_long": "通報理由が長すぎます。",
|
||||||
|
"@report_too_long": {},
|
||||||
|
"couldnt_create_report": "通報を作成できませんでした。",
|
||||||
|
"@couldnt_create_report": {},
|
||||||
|
"couldnt_resolve_report": "通報を解決できませんでした。",
|
||||||
|
"@couldnt_resolve_report": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,131 +1,268 @@
|
||||||
{
|
{
|
||||||
"@@locale": "pl",
|
"@@locale": "pl",
|
||||||
"settings": "Ustawienia",
|
"settings": "Ustawieniad",
|
||||||
|
"@settings": {},
|
||||||
"password": "Hasło",
|
"password": "Hasło",
|
||||||
|
"@password": {},
|
||||||
"email_or_username": "Email lub Login",
|
"email_or_username": "Email lub Login",
|
||||||
|
"@email_or_username": {},
|
||||||
"posts": "Posty",
|
"posts": "Posty",
|
||||||
|
"@posts": {},
|
||||||
"comments": "Komentarze",
|
"comments": "Komentarze",
|
||||||
|
"@comments": {},
|
||||||
"modlog": "Log moderatorski",
|
"modlog": "Log moderatorski",
|
||||||
|
"@modlog": {},
|
||||||
"community": "Społeczność",
|
"community": "Społeczność",
|
||||||
|
"@community": {},
|
||||||
"url": "Link",
|
"url": "Link",
|
||||||
|
"@url": {},
|
||||||
"title": "Tytuł",
|
"title": "Tytuł",
|
||||||
|
"@title": {},
|
||||||
"body": "Treść",
|
"body": "Treść",
|
||||||
|
"@body": {},
|
||||||
"nsfw": "NSFW (18+)",
|
"nsfw": "NSFW (18+)",
|
||||||
|
"@nsfw": {},
|
||||||
"post": "post",
|
"post": "post",
|
||||||
|
"@post": {},
|
||||||
"save": "zapisz",
|
"save": "zapisz",
|
||||||
|
"@save": {},
|
||||||
"subscribed": "Subskrybowane",
|
"subscribed": "Subskrybowane",
|
||||||
|
"@subscribed": {},
|
||||||
"local": "Lokalne",
|
"local": "Lokalne",
|
||||||
|
"@local": {},
|
||||||
"all": "Wszystko",
|
"all": "Wszystko",
|
||||||
|
"@all": {},
|
||||||
"replies": "Odpowiedzi",
|
"replies": "Odpowiedzi",
|
||||||
|
"@replies": {},
|
||||||
"mentions": "Wzmianki",
|
"mentions": "Wzmianki",
|
||||||
|
"@mentions": {},
|
||||||
"from": "od",
|
"from": "od",
|
||||||
|
"@from": {},
|
||||||
"to": "do",
|
"to": "do",
|
||||||
|
"@to": {},
|
||||||
"deleted_by_creator": "usunięte przez osobę publikującą",
|
"deleted_by_creator": "usunięte przez osobę publikującą",
|
||||||
|
"@deleted_by_creator": {},
|
||||||
"more": "więcej",
|
"more": "więcej",
|
||||||
|
"@more": {},
|
||||||
"mark_as_read": "zaznacz jako przeczytane",
|
"mark_as_read": "zaznacz jako przeczytane",
|
||||||
|
"@mark_as_read": {},
|
||||||
"mark_as_unread": "zaznacz jako nieprzeczytane",
|
"mark_as_unread": "zaznacz jako nieprzeczytane",
|
||||||
|
"@mark_as_unread": {},
|
||||||
"reply": "odpowiedz",
|
"reply": "odpowiedz",
|
||||||
|
"@reply": {},
|
||||||
"edit": "edytuj",
|
"edit": "edytuj",
|
||||||
|
"@edit": {},
|
||||||
"delete": "usuń",
|
"delete": "usuń",
|
||||||
|
"@delete": {},
|
||||||
"restore": "przywróć",
|
"restore": "przywróć",
|
||||||
|
"@restore": {},
|
||||||
"yes": "tak",
|
"yes": "tak",
|
||||||
|
"@yes": {},
|
||||||
"no": "nie",
|
"no": "nie",
|
||||||
|
"@no": {},
|
||||||
"avatar": "Awatar",
|
"avatar": "Awatar",
|
||||||
|
"@avatar": {},
|
||||||
"banner": "Baner",
|
"banner": "Baner",
|
||||||
|
"@banner": {},
|
||||||
"display_name": "Nazwa wyświetlana",
|
"display_name": "Nazwa wyświetlana",
|
||||||
|
"@display_name": {},
|
||||||
"bio": "Opis",
|
"bio": "Opis",
|
||||||
|
"@bio": {},
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"@email": {},
|
||||||
"matrix_user": "Login na Matrixie",
|
"matrix_user": "Login na Matrixie",
|
||||||
|
"@matrix_user": {},
|
||||||
"sort_type": "Sortuj typ",
|
"sort_type": "Sortuj typ",
|
||||||
|
"@sort_type": {},
|
||||||
"type": "Rodzaj",
|
"type": "Rodzaj",
|
||||||
|
"@type": {},
|
||||||
"show_nsfw": "Pokaż treści NSFW (18+)",
|
"show_nsfw": "Pokaż treści NSFW (18+)",
|
||||||
|
"@show_nsfw": {},
|
||||||
"send_notifications_to_email": "Wysyłaj powiadomienia na Email",
|
"send_notifications_to_email": "Wysyłaj powiadomienia na Email",
|
||||||
|
"@send_notifications_to_email": {},
|
||||||
"delete_account": "Usuń Konto",
|
"delete_account": "Usuń Konto",
|
||||||
|
"@delete_account": {},
|
||||||
"saved": "Zapisane",
|
"saved": "Zapisane",
|
||||||
|
"@saved": {},
|
||||||
"communities": "Społeczności",
|
"communities": "Społeczności",
|
||||||
|
"@communities": {},
|
||||||
"users": "Osoby zalogowane",
|
"users": "Osoby zalogowane",
|
||||||
|
"@users": {},
|
||||||
"theme": "Motyw",
|
"theme": "Motyw",
|
||||||
|
"@theme": {},
|
||||||
"language": "Język",
|
"language": "Język",
|
||||||
|
"@language": {},
|
||||||
"hot": "Popularne",
|
"hot": "Popularne",
|
||||||
|
"@hot": {},
|
||||||
"new_": "Nowe",
|
"new_": "Nowe",
|
||||||
|
"@new_": {},
|
||||||
"old": "Stare",
|
"old": "Stare",
|
||||||
|
"@old": {},
|
||||||
"top": "Najpopularniejsze",
|
"top": "Najpopularniejsze",
|
||||||
|
"@top": {},
|
||||||
"chat": "Dyskusja",
|
"chat": "Dyskusja",
|
||||||
|
"@chat": {},
|
||||||
"admin": "admin",
|
"admin": "admin",
|
||||||
|
"@admin": {},
|
||||||
"by": "przez",
|
"by": "przez",
|
||||||
|
"@by": {},
|
||||||
"not_a_mod_or_admin": "Nie moderuje ani administruje.",
|
"not_a_mod_or_admin": "Nie moderuje ani administruje.",
|
||||||
|
"@not_a_mod_or_admin": {},
|
||||||
"not_an_admin": "Nie jest administratorem.",
|
"not_an_admin": "Nie jest administratorem.",
|
||||||
|
"@not_an_admin": {},
|
||||||
"couldnt_find_post": "Nie udało się znaleźć posta.",
|
"couldnt_find_post": "Nie udało się znaleźć posta.",
|
||||||
|
"@couldnt_find_post": {},
|
||||||
"not_logged_in": "Nie jesteś zalogowana/y.",
|
"not_logged_in": "Nie jesteś zalogowana/y.",
|
||||||
|
"@not_logged_in": {},
|
||||||
"site_ban": "Zostałaś/eś zbanowana/y z tej witryny",
|
"site_ban": "Zostałaś/eś zbanowana/y z tej witryny",
|
||||||
|
"@site_ban": {},
|
||||||
"community_ban": "Zostałaś/eś zbanowana/y z tej społeczności.",
|
"community_ban": "Zostałaś/eś zbanowana/y z tej społeczności.",
|
||||||
|
"@community_ban": {},
|
||||||
"downvotes_disabled": "Wdółgłosy wyłączone",
|
"downvotes_disabled": "Wdółgłosy wyłączone",
|
||||||
|
"@downvotes_disabled": {},
|
||||||
"invalid_url": "Nieprawidłowy link.",
|
"invalid_url": "Nieprawidłowy link.",
|
||||||
|
"@invalid_url": {},
|
||||||
"locked": "zablokowane",
|
"locked": "zablokowane",
|
||||||
|
"@locked": {},
|
||||||
"couldnt_create_comment": "Nie udało się stworzyć komentarza.",
|
"couldnt_create_comment": "Nie udało się stworzyć komentarza.",
|
||||||
|
"@couldnt_create_comment": {},
|
||||||
"couldnt_like_comment": "Polubienie komentarza nie powiodło się.",
|
"couldnt_like_comment": "Polubienie komentarza nie powiodło się.",
|
||||||
|
"@couldnt_like_comment": {},
|
||||||
"couldnt_update_comment": "Zaktualizowanie komentarza nie powiodło się.",
|
"couldnt_update_comment": "Zaktualizowanie komentarza nie powiodło się.",
|
||||||
|
"@couldnt_update_comment": {},
|
||||||
"no_comment_edit_allowed": "Nie masz uprawnień do edycji komentarza.",
|
"no_comment_edit_allowed": "Nie masz uprawnień do edycji komentarza.",
|
||||||
|
"@no_comment_edit_allowed": {},
|
||||||
"couldnt_save_comment": "Zapisanie komentarza nie powiodło się.",
|
"couldnt_save_comment": "Zapisanie komentarza nie powiodło się.",
|
||||||
|
"@couldnt_save_comment": {},
|
||||||
"couldnt_get_comments": "Pobranie komentarzy nie powiodło się.",
|
"couldnt_get_comments": "Pobranie komentarzy nie powiodło się.",
|
||||||
|
"@couldnt_get_comments": {},
|
||||||
"report_reason_required": "Wymagane jest uzasadnienie zgłoszenia.",
|
"report_reason_required": "Wymagane jest uzasadnienie zgłoszenia.",
|
||||||
|
"@report_reason_required": {},
|
||||||
"report_too_long": "Zgłoszenie jest zbyt długie.",
|
"report_too_long": "Zgłoszenie jest zbyt długie.",
|
||||||
|
"@report_too_long": {},
|
||||||
"couldnt_create_report": "Nie udało się stworzyć zgłoszenia.",
|
"couldnt_create_report": "Nie udało się stworzyć zgłoszenia.",
|
||||||
|
"@couldnt_create_report": {},
|
||||||
"couldnt_resolve_report": "Nie udało się rozwiązać zgłoszenia.",
|
"couldnt_resolve_report": "Nie udało się rozwiązać zgłoszenia.",
|
||||||
|
"@couldnt_resolve_report": {},
|
||||||
"invalid_post_title": "Nieprawidłowy tytuł posta",
|
"invalid_post_title": "Nieprawidłowy tytuł posta",
|
||||||
|
"@invalid_post_title": {},
|
||||||
"couldnt_create_post": "Nie udało się stworzyć posta.",
|
"couldnt_create_post": "Nie udało się stworzyć posta.",
|
||||||
|
"@couldnt_create_post": {},
|
||||||
"couldnt_like_post": "Nie udało się polubić posta.",
|
"couldnt_like_post": "Nie udało się polubić posta.",
|
||||||
|
"@couldnt_like_post": {},
|
||||||
"couldnt_find_community": "Nie udało się znaleźć społeczności.",
|
"couldnt_find_community": "Nie udało się znaleźć społeczności.",
|
||||||
|
"@couldnt_find_community": {},
|
||||||
"couldnt_get_posts": "Nie udało się pobrać postów",
|
"couldnt_get_posts": "Nie udało się pobrać postów",
|
||||||
|
"@couldnt_get_posts": {},
|
||||||
"no_post_edit_allowed": "Nie masz uprawnień do edycji posta.",
|
"no_post_edit_allowed": "Nie masz uprawnień do edycji posta.",
|
||||||
|
"@no_post_edit_allowed": {},
|
||||||
"couldnt_save_post": "Nie udało się zapisać posta.",
|
"couldnt_save_post": "Nie udało się zapisać posta.",
|
||||||
|
"@couldnt_save_post": {},
|
||||||
"site_already_exists": "Witryna już istnieje.",
|
"site_already_exists": "Witryna już istnieje.",
|
||||||
|
"@site_already_exists": {},
|
||||||
"couldnt_update_site": "Nie udało się zaktualizować witryny.",
|
"couldnt_update_site": "Nie udało się zaktualizować witryny.",
|
||||||
|
"@couldnt_update_site": {},
|
||||||
"invalid_community_name": "Niepoprawna nazwa.",
|
"invalid_community_name": "Niepoprawna nazwa.",
|
||||||
|
"@invalid_community_name": {},
|
||||||
"community_already_exists": "Społeczność już istnieje.",
|
"community_already_exists": "Społeczność już istnieje.",
|
||||||
|
"@community_already_exists": {},
|
||||||
"community_moderator_already_exists": "Moderator społeczności już istnieje.",
|
"community_moderator_already_exists": "Moderator społeczności już istnieje.",
|
||||||
|
"@community_moderator_already_exists": {},
|
||||||
"community_follower_already_exists": "Osoba obserwująca społeczność już istnieje.",
|
"community_follower_already_exists": "Osoba obserwująca społeczność już istnieje.",
|
||||||
|
"@community_follower_already_exists": {},
|
||||||
"not_a_moderator": "Nie jest moderatorem.",
|
"not_a_moderator": "Nie jest moderatorem.",
|
||||||
|
"@not_a_moderator": {},
|
||||||
"couldnt_update_community": "Nie udało się zaktualizować Społeczności.",
|
"couldnt_update_community": "Nie udało się zaktualizować Społeczności.",
|
||||||
|
"@couldnt_update_community": {},
|
||||||
"no_community_edit_allowed": "Nie masz uprawnień do edycji społeczności.",
|
"no_community_edit_allowed": "Nie masz uprawnień do edycji społeczności.",
|
||||||
|
"@no_community_edit_allowed": {},
|
||||||
"system_err_login": "Błąd systemu. Spróbuj wylogować się i następnie zalogować ponownie.",
|
"system_err_login": "Błąd systemu. Spróbuj wylogować się i następnie zalogować ponownie.",
|
||||||
|
"@system_err_login": {},
|
||||||
"community_user_already_banned": "Login już zablokowany w tej społeczności.",
|
"community_user_already_banned": "Login już zablokowany w tej społeczności.",
|
||||||
|
"@community_user_already_banned": {},
|
||||||
"couldnt_find_that_username_or_email": "Nie udało się znaleźć takiego loginu lub adresu email.",
|
"couldnt_find_that_username_or_email": "Nie udało się znaleźć takiego loginu lub adresu email.",
|
||||||
|
"@couldnt_find_that_username_or_email": {},
|
||||||
"password_incorrect": "Hasło niepoprawne.",
|
"password_incorrect": "Hasło niepoprawne.",
|
||||||
|
"@password_incorrect": {},
|
||||||
"registration_closed": "Rejestracja Zamknięta",
|
"registration_closed": "Rejestracja Zamknięta",
|
||||||
|
"@registration_closed": {},
|
||||||
"invalid_password": "Nieprawidłowe hasło. Hasło musi mieć mniej niż 60 znaków.",
|
"invalid_password": "Nieprawidłowe hasło. Hasło musi mieć mniej niż 60 znaków.",
|
||||||
|
"@invalid_password": {},
|
||||||
"passwords_dont_match": "Hasła nie pasują do siebie.",
|
"passwords_dont_match": "Hasła nie pasują do siebie.",
|
||||||
|
"@passwords_dont_match": {},
|
||||||
"captcha_incorrect": "Captcha niepoprawna.",
|
"captcha_incorrect": "Captcha niepoprawna.",
|
||||||
|
"@captcha_incorrect": {},
|
||||||
"invalid_username": "Nieprawidłowy login.",
|
"invalid_username": "Nieprawidłowy login.",
|
||||||
|
"@invalid_username": {},
|
||||||
"bio_length_overflow": "To pole nie może przekraczać 300 znaków.",
|
"bio_length_overflow": "To pole nie może przekraczać 300 znaków.",
|
||||||
|
"@bio_length_overflow": {},
|
||||||
"couldnt_update_user": "Nie udało się zaktualizować.",
|
"couldnt_update_user": "Nie udało się zaktualizować.",
|
||||||
|
"@couldnt_update_user": {},
|
||||||
"couldnt_update_private_message": "Nie udało się zaktualizować prywatnej wiadomości.",
|
"couldnt_update_private_message": "Nie udało się zaktualizować prywatnej wiadomości.",
|
||||||
|
"@couldnt_update_private_message": {},
|
||||||
"couldnt_update_post": "Nie udało się zaktualizować postów",
|
"couldnt_update_post": "Nie udało się zaktualizować postów",
|
||||||
|
"@couldnt_update_post": {},
|
||||||
"couldnt_create_private_message": "Nie udało się stworzyć prywatnej wiadomości.",
|
"couldnt_create_private_message": "Nie udało się stworzyć prywatnej wiadomości.",
|
||||||
|
"@couldnt_create_private_message": {},
|
||||||
"no_private_message_edit_allowed": "Brak uprawnień do edycji prywatnej wiadomości.",
|
"no_private_message_edit_allowed": "Brak uprawnień do edycji prywatnej wiadomości.",
|
||||||
|
"@no_private_message_edit_allowed": {},
|
||||||
"post_title_too_long": "Tytuł posta zbyt długi.",
|
"post_title_too_long": "Tytuł posta zbyt długi.",
|
||||||
|
"@post_title_too_long": {},
|
||||||
"email_already_exists": "Email już istnieje.",
|
"email_already_exists": "Email już istnieje.",
|
||||||
|
"@email_already_exists": {},
|
||||||
"user_already_exists": "Login już istnieje.",
|
"user_already_exists": "Login już istnieje.",
|
||||||
|
"@user_already_exists": {},
|
||||||
"unsubscribe": "Odsubskrybuj",
|
"unsubscribe": "Odsubskrybuj",
|
||||||
|
"@unsubscribe": {},
|
||||||
"subscribe": "Subskrybuj",
|
"subscribe": "Subskrybuj",
|
||||||
|
"@subscribe": {},
|
||||||
"messages": "Wiadomości",
|
"messages": "Wiadomości",
|
||||||
|
"@messages": {},
|
||||||
"banned_users": "Zbanowani Użytkownicy",
|
"banned_users": "Zbanowani Użytkownicy",
|
||||||
|
"@banned_users": {},
|
||||||
"delete_account_confirm": "Ostrzeżenie: twoje dane zostaną bezpowrotnie usunięte na tej instancji. Mogą one pozostać na innych instancjach. Wpisz swoje hasło aby potwierdzić.",
|
"delete_account_confirm": "Ostrzeżenie: twoje dane zostaną bezpowrotnie usunięte na tej instancji. Mogą one pozostać na innych instancjach. Wpisz swoje hasło aby potwierdzić.",
|
||||||
|
"@delete_account_confirm": {},
|
||||||
"new_password": "Nowe Hasło",
|
"new_password": "Nowe Hasło",
|
||||||
|
"@new_password": {},
|
||||||
"verify_password": "Zweryfikuj Hasło",
|
"verify_password": "Zweryfikuj Hasło",
|
||||||
|
"@verify_password": {},
|
||||||
"old_password": "Stare Hasło",
|
"old_password": "Stare Hasło",
|
||||||
|
"@old_password": {},
|
||||||
"show_avatars": "Pokaż Awatary",
|
"show_avatars": "Pokaż Awatary",
|
||||||
|
"@show_avatars": {},
|
||||||
"search": "Szukaj",
|
"search": "Szukaj",
|
||||||
|
"@search": {},
|
||||||
"send_message": "Wyślij Wiadomość",
|
"send_message": "Wyślij Wiadomość",
|
||||||
|
"@send_message": {},
|
||||||
"top_day": "Najpopularniejsze dziś",
|
"top_day": "Najpopularniejsze dziś",
|
||||||
|
"@top_day": {},
|
||||||
"top_week": "Najpopularniejsze tydzień",
|
"top_week": "Najpopularniejsze tydzień",
|
||||||
|
"@top_week": {},
|
||||||
"top_month": "Najpopularniejsze miesiąc",
|
"top_month": "Najpopularniejsze miesiąc",
|
||||||
|
"@top_month": {},
|
||||||
"top_year": "Najpopularniejsze rok",
|
"top_year": "Najpopularniejsze rok",
|
||||||
|
"@top_year": {},
|
||||||
"top_all": "Najpopularniejsze kiedykolwiek",
|
"top_all": "Najpopularniejsze kiedykolwiek",
|
||||||
|
"@top_all": {},
|
||||||
"most_comments": "Najwięcej komentarzy",
|
"most_comments": "Najwięcej komentarzy",
|
||||||
|
"@most_comments": {},
|
||||||
"new_comments": "Nowe komentarze",
|
"new_comments": "Nowe komentarze",
|
||||||
|
"@new_comments": {},
|
||||||
"active": "Aktywne",
|
"active": "Aktywne",
|
||||||
|
"@active": {},
|
||||||
"bot_account": "Konto bota",
|
"bot_account": "Konto bota",
|
||||||
|
"@bot_account": {},
|
||||||
"show_bot_accounts": "Pokaż konta botów",
|
"show_bot_accounts": "Pokaż konta botów",
|
||||||
"show_read_posts": "Pokaż przeczytane posty"
|
"@show_bot_accounts": {},
|
||||||
|
"show_read_posts": "Pokaż przeczytane posty",
|
||||||
|
"@show_read_posts": {},
|
||||||
|
"number_of_posts": "{formattedCount,plural, =1{1 post} other{{formattedCount} postów}}",
|
||||||
|
"@number_of_posts": {
|
||||||
|
"placeholders": {
|
||||||
|
"formattedCount": {
|
||||||
|
"type": "int",
|
||||||
|
"format": "compact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,128 +1,269 @@
|
||||||
{
|
{
|
||||||
"@@locale": "pt_BR",
|
"@@locale": "pt_BR",
|
||||||
"settings": "Configurações",
|
"settings": "Configurações",
|
||||||
|
"@settings": {},
|
||||||
"password": "Senha",
|
"password": "Senha",
|
||||||
|
"@password": {},
|
||||||
"email_or_username": "E-mail ou nome de usuário",
|
"email_or_username": "E-mail ou nome de usuário",
|
||||||
|
"@email_or_username": {},
|
||||||
"posts": "Publicações",
|
"posts": "Publicações",
|
||||||
|
"@posts": {},
|
||||||
"comments": "Comentários",
|
"comments": "Comentários",
|
||||||
|
"@comments": {},
|
||||||
"modlog": "Registro de moderação",
|
"modlog": "Registro de moderação",
|
||||||
|
"@modlog": {},
|
||||||
"community": "Comunidade",
|
"community": "Comunidade",
|
||||||
|
"@community": {},
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"@url": {},
|
||||||
"title": "Título",
|
"title": "Título",
|
||||||
|
"@title": {},
|
||||||
"body": "Conteúdo",
|
"body": "Conteúdo",
|
||||||
|
"@body": {},
|
||||||
"nsfw": "NSFW",
|
"nsfw": "NSFW",
|
||||||
|
"@nsfw": {},
|
||||||
"post": "publicação",
|
"post": "publicação",
|
||||||
|
"@post": {},
|
||||||
"save": "guardar",
|
"save": "guardar",
|
||||||
|
"@save": {},
|
||||||
"subscribed": "Inscrito",
|
"subscribed": "Inscrito",
|
||||||
|
"@subscribed": {},
|
||||||
"local": "Local",
|
"local": "Local",
|
||||||
|
"@local": {},
|
||||||
"all": "Tudo",
|
"all": "Tudo",
|
||||||
|
"@all": {},
|
||||||
"replies": "Respostas",
|
"replies": "Respostas",
|
||||||
|
"@replies": {},
|
||||||
"mentions": "Menções",
|
"mentions": "Menções",
|
||||||
|
"@mentions": {},
|
||||||
"from": "de",
|
"from": "de",
|
||||||
|
"@from": {},
|
||||||
"to": "para",
|
"to": "para",
|
||||||
|
"@to": {},
|
||||||
"deleted_by_creator": "apagado pelo criador",
|
"deleted_by_creator": "apagado pelo criador",
|
||||||
|
"@deleted_by_creator": {},
|
||||||
"more": "mais",
|
"more": "mais",
|
||||||
|
"@more": {},
|
||||||
"mark_as_read": "marcar como lido",
|
"mark_as_read": "marcar como lido",
|
||||||
|
"@mark_as_read": {},
|
||||||
"mark_as_unread": "marcar como não lido",
|
"mark_as_unread": "marcar como não lido",
|
||||||
|
"@mark_as_unread": {},
|
||||||
"reply": "responder",
|
"reply": "responder",
|
||||||
|
"@reply": {},
|
||||||
"edit": "editar",
|
"edit": "editar",
|
||||||
|
"@edit": {},
|
||||||
"delete": "apagar",
|
"delete": "apagar",
|
||||||
|
"@delete": {},
|
||||||
"restore": "restaurar",
|
"restore": "restaurar",
|
||||||
|
"@restore": {},
|
||||||
"yes": "sim",
|
"yes": "sim",
|
||||||
|
"@yes": {},
|
||||||
"no": "não",
|
"no": "não",
|
||||||
|
"@no": {},
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
|
"@avatar": {},
|
||||||
"banner": "Banner",
|
"banner": "Banner",
|
||||||
|
"@banner": {},
|
||||||
"display_name": "Nome de exibição",
|
"display_name": "Nome de exibição",
|
||||||
|
"@display_name": {},
|
||||||
"bio": "Biografia",
|
"bio": "Biografia",
|
||||||
|
"@bio": {},
|
||||||
"email": "E-mail",
|
"email": "E-mail",
|
||||||
|
"@email": {},
|
||||||
"matrix_user": "Usuário Matrix",
|
"matrix_user": "Usuário Matrix",
|
||||||
|
"@matrix_user": {},
|
||||||
"sort_type": "Ordenação",
|
"sort_type": "Ordenação",
|
||||||
|
"@sort_type": {},
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
|
"@type": {},
|
||||||
"show_nsfw": "Mostrar conteúdo NSFW",
|
"show_nsfw": "Mostrar conteúdo NSFW",
|
||||||
|
"@show_nsfw": {},
|
||||||
"send_notifications_to_email": "Enviar notificações para o e-mail",
|
"send_notifications_to_email": "Enviar notificações para o e-mail",
|
||||||
|
"@send_notifications_to_email": {},
|
||||||
"delete_account": "Apagar conta",
|
"delete_account": "Apagar conta",
|
||||||
|
"@delete_account": {},
|
||||||
"saved": "Guardado",
|
"saved": "Guardado",
|
||||||
|
"@saved": {},
|
||||||
"communities": "Comunidades",
|
"communities": "Comunidades",
|
||||||
|
"@communities": {},
|
||||||
"users": "Usuários",
|
"users": "Usuários",
|
||||||
|
"@users": {},
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
|
"@theme": {},
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
|
"@language": {},
|
||||||
"hot": "Popular",
|
"hot": "Popular",
|
||||||
|
"@hot": {},
|
||||||
"new_": "Novo",
|
"new_": "Novo",
|
||||||
|
"@new_": {},
|
||||||
"old": "Velho",
|
"old": "Velho",
|
||||||
|
"@old": {},
|
||||||
"top": "Top",
|
"top": "Top",
|
||||||
|
"@top": {},
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"@chat": {},
|
||||||
"admin": "administrador",
|
"admin": "administrador",
|
||||||
|
"@admin": {},
|
||||||
"by": "por",
|
"by": "por",
|
||||||
|
"@by": {},
|
||||||
"not_a_mod_or_admin": "Não é moderador ou administrador.",
|
"not_a_mod_or_admin": "Não é moderador ou administrador.",
|
||||||
|
"@not_a_mod_or_admin": {},
|
||||||
"not_an_admin": "Não é administrador.",
|
"not_an_admin": "Não é administrador.",
|
||||||
|
"@not_an_admin": {},
|
||||||
"couldnt_find_post": "Não foi possível encontrar a publicação.",
|
"couldnt_find_post": "Não foi possível encontrar a publicação.",
|
||||||
|
"@couldnt_find_post": {},
|
||||||
"not_logged_in": "Não autenticado.",
|
"not_logged_in": "Não autenticado.",
|
||||||
|
"@not_logged_in": {},
|
||||||
"site_ban": "Você foi banido do site",
|
"site_ban": "Você foi banido do site",
|
||||||
|
"@site_ban": {},
|
||||||
"community_ban": "Você foi banido desta comunidade.",
|
"community_ban": "Você foi banido desta comunidade.",
|
||||||
|
"@community_ban": {},
|
||||||
"downvotes_disabled": "Votos negativos desativados",
|
"downvotes_disabled": "Votos negativos desativados",
|
||||||
|
"@downvotes_disabled": {},
|
||||||
"invalid_url": "URL inválida.",
|
"invalid_url": "URL inválida.",
|
||||||
|
"@invalid_url": {},
|
||||||
"locked": "trancado",
|
"locked": "trancado",
|
||||||
|
"@locked": {},
|
||||||
"couldnt_create_comment": "Não foi possível criar o comentário.",
|
"couldnt_create_comment": "Não foi possível criar o comentário.",
|
||||||
|
"@couldnt_create_comment": {},
|
||||||
"couldnt_like_comment": "Não foi possível curtir o comentário.",
|
"couldnt_like_comment": "Não foi possível curtir o comentário.",
|
||||||
|
"@couldnt_like_comment": {},
|
||||||
"couldnt_update_comment": "Não foi possível atualizar o comentário.",
|
"couldnt_update_comment": "Não foi possível atualizar o comentário.",
|
||||||
|
"@couldnt_update_comment": {},
|
||||||
"no_comment_edit_allowed": "Sem permissão para editar comentário.",
|
"no_comment_edit_allowed": "Sem permissão para editar comentário.",
|
||||||
|
"@no_comment_edit_allowed": {},
|
||||||
"couldnt_save_comment": "Não foi possível salvar o comentário.",
|
"couldnt_save_comment": "Não foi possível salvar o comentário.",
|
||||||
|
"@couldnt_save_comment": {},
|
||||||
"couldnt_get_comments": "Não foi possível obter os comentários.",
|
"couldnt_get_comments": "Não foi possível obter os comentários.",
|
||||||
|
"@couldnt_get_comments": {},
|
||||||
"invalid_post_title": "Título de publicação inválido",
|
"invalid_post_title": "Título de publicação inválido",
|
||||||
|
"@invalid_post_title": {},
|
||||||
"couldnt_create_post": "Não foi possível criar a publicação.",
|
"couldnt_create_post": "Não foi possível criar a publicação.",
|
||||||
|
"@couldnt_create_post": {},
|
||||||
"couldnt_like_post": "Não foi possível curtir a publicação.",
|
"couldnt_like_post": "Não foi possível curtir a publicação.",
|
||||||
|
"@couldnt_like_post": {},
|
||||||
"couldnt_find_community": "Não foi possível encontrar a comunidade.",
|
"couldnt_find_community": "Não foi possível encontrar a comunidade.",
|
||||||
|
"@couldnt_find_community": {},
|
||||||
"couldnt_get_posts": "Não foi possível obter as publicações",
|
"couldnt_get_posts": "Não foi possível obter as publicações",
|
||||||
|
"@couldnt_get_posts": {},
|
||||||
"no_post_edit_allowed": "Sem permissão para editar publicação.",
|
"no_post_edit_allowed": "Sem permissão para editar publicação.",
|
||||||
|
"@no_post_edit_allowed": {},
|
||||||
"couldnt_save_post": "Não foi possível guardar a publicação.",
|
"couldnt_save_post": "Não foi possível guardar a publicação.",
|
||||||
|
"@couldnt_save_post": {},
|
||||||
"site_already_exists": "O site já existe.",
|
"site_already_exists": "O site já existe.",
|
||||||
|
"@site_already_exists": {},
|
||||||
"couldnt_update_site": "Não foi possível atualizar o site.",
|
"couldnt_update_site": "Não foi possível atualizar o site.",
|
||||||
|
"@couldnt_update_site": {},
|
||||||
"invalid_community_name": "Nome inválido.",
|
"invalid_community_name": "Nome inválido.",
|
||||||
|
"@invalid_community_name": {},
|
||||||
"community_already_exists": "Esta comunidade já existe.",
|
"community_already_exists": "Esta comunidade já existe.",
|
||||||
|
"@community_already_exists": {},
|
||||||
"community_moderator_already_exists": "Este moderador da comunidade já existe.",
|
"community_moderator_already_exists": "Este moderador da comunidade já existe.",
|
||||||
|
"@community_moderator_already_exists": {},
|
||||||
"community_follower_already_exists": "Este seguidor da comunidade já existe.",
|
"community_follower_already_exists": "Este seguidor da comunidade já existe.",
|
||||||
|
"@community_follower_already_exists": {},
|
||||||
"not_a_moderator": "Não é um(a) moderador(a).",
|
"not_a_moderator": "Não é um(a) moderador(a).",
|
||||||
|
"@not_a_moderator": {},
|
||||||
"couldnt_update_community": "Não foi possível atualizar a comunidade.",
|
"couldnt_update_community": "Não foi possível atualizar a comunidade.",
|
||||||
|
"@couldnt_update_community": {},
|
||||||
"no_community_edit_allowed": "Sem permissão para editar comunidade.",
|
"no_community_edit_allowed": "Sem permissão para editar comunidade.",
|
||||||
|
"@no_community_edit_allowed": {},
|
||||||
"system_err_login": "Erro no sistema. Tente sair e autenticar-se outra vez.",
|
"system_err_login": "Erro no sistema. Tente sair e autenticar-se outra vez.",
|
||||||
|
"@system_err_login": {},
|
||||||
"community_user_already_banned": "Este usuário da comunidade já foi banido.",
|
"community_user_already_banned": "Este usuário da comunidade já foi banido.",
|
||||||
|
"@community_user_already_banned": {},
|
||||||
"couldnt_find_that_username_or_email": "Não foi possível encontrar esse usuário ou e-mail.",
|
"couldnt_find_that_username_or_email": "Não foi possível encontrar esse usuário ou e-mail.",
|
||||||
|
"@couldnt_find_that_username_or_email": {},
|
||||||
"password_incorrect": "Senha incorreta.",
|
"password_incorrect": "Senha incorreta.",
|
||||||
|
"@password_incorrect": {},
|
||||||
"registration_closed": "Registros desativados",
|
"registration_closed": "Registros desativados",
|
||||||
|
"@registration_closed": {},
|
||||||
"invalid_password": "Senha inválida. A senha deve ter no máximo 60 caracteres.",
|
"invalid_password": "Senha inválida. A senha deve ter no máximo 60 caracteres.",
|
||||||
|
"@invalid_password": {},
|
||||||
"passwords_dont_match": "As senhas não são iguais.",
|
"passwords_dont_match": "As senhas não são iguais.",
|
||||||
|
"@passwords_dont_match": {},
|
||||||
"captcha_incorrect": "Captcha incorreto.",
|
"captcha_incorrect": "Captcha incorreto.",
|
||||||
|
"@captcha_incorrect": {},
|
||||||
"invalid_username": "Nome de usuário inválido.",
|
"invalid_username": "Nome de usuário inválido.",
|
||||||
|
"@invalid_username": {},
|
||||||
"bio_length_overflow": "Uma biografia de usuário não pode ter mais de 300 caracters.",
|
"bio_length_overflow": "Uma biografia de usuário não pode ter mais de 300 caracters.",
|
||||||
|
"@bio_length_overflow": {},
|
||||||
"couldnt_update_user": "Não foi possível atualizar o usuário.",
|
"couldnt_update_user": "Não foi possível atualizar o usuário.",
|
||||||
|
"@couldnt_update_user": {},
|
||||||
"couldnt_update_private_message": "Não foi possível atualizar a mensagem privada.",
|
"couldnt_update_private_message": "Não foi possível atualizar a mensagem privada.",
|
||||||
|
"@couldnt_update_private_message": {},
|
||||||
"couldnt_update_post": "Não foi possível atualizar a publicação",
|
"couldnt_update_post": "Não foi possível atualizar a publicação",
|
||||||
|
"@couldnt_update_post": {},
|
||||||
"couldnt_create_private_message": "Não foi possível criar mensagem privada.",
|
"couldnt_create_private_message": "Não foi possível criar mensagem privada.",
|
||||||
|
"@couldnt_create_private_message": {},
|
||||||
"no_private_message_edit_allowed": "Sem permissão para editar mensagem privada.",
|
"no_private_message_edit_allowed": "Sem permissão para editar mensagem privada.",
|
||||||
|
"@no_private_message_edit_allowed": {},
|
||||||
"post_title_too_long": "Título da publicação muito longo.",
|
"post_title_too_long": "Título da publicação muito longo.",
|
||||||
|
"@post_title_too_long": {},
|
||||||
"email_already_exists": "Este e-mail já existe.",
|
"email_already_exists": "Este e-mail já existe.",
|
||||||
|
"@email_already_exists": {},
|
||||||
"user_already_exists": "Este usuário já existe.",
|
"user_already_exists": "Este usuário já existe.",
|
||||||
|
"@user_already_exists": {},
|
||||||
"number_of_users_online": "{formattedCount,plural, =1{{formattedCount} usuário online} other{{formattedCount} usuários online}}",
|
"number_of_users_online": "{formattedCount,plural, =1{{formattedCount} usuário online} other{{formattedCount} usuários online}}",
|
||||||
|
"@number_of_users_online": {},
|
||||||
"number_of_comments": "{formattedCount,plural, =1{{formattedCount} comentário} other{{formattedCount} comentários}}",
|
"number_of_comments": "{formattedCount,plural, =1{{formattedCount} comentário} other{{formattedCount} comentários}}",
|
||||||
|
"@number_of_comments": {},
|
||||||
"number_of_posts": "{formattedCount,plural, =1{{formattedCount} publicação} other{{formattedCount} publicações}}",
|
"number_of_posts": "{formattedCount,plural, =1{{formattedCount} publicação} other{{formattedCount} publicações}}",
|
||||||
|
"@number_of_posts": {},
|
||||||
"number_of_subscribers": "{formattedCount,plural, =1{{formattedCount} inscrito} other{{formattedCount} inscritos}}",
|
"number_of_subscribers": "{formattedCount,plural, =1{{formattedCount} inscrito} other{{formattedCount} inscritos}}",
|
||||||
|
"@number_of_subscribers": {},
|
||||||
"number_of_users": "{formattedCount,plural, =1{{formattedCount} usuário} other{{formattedCount} usuários}}",
|
"number_of_users": "{formattedCount,plural, =1{{formattedCount} usuário} other{{formattedCount} usuários}}",
|
||||||
|
"@number_of_users": {},
|
||||||
"unsubscribe": "Cancelar inscrição",
|
"unsubscribe": "Cancelar inscrição",
|
||||||
|
"@unsubscribe": {},
|
||||||
"subscribe": "Inscrever-se",
|
"subscribe": "Inscrever-se",
|
||||||
|
"@subscribe": {},
|
||||||
"messages": "Mensagens",
|
"messages": "Mensagens",
|
||||||
|
"@messages": {},
|
||||||
"banned_users": "Usuários Banidos",
|
"banned_users": "Usuários Banidos",
|
||||||
|
"@banned_users": {},
|
||||||
"delete_account_confirm": "Aviso: isso vai apagar seus dados de forma permanente. Escreva sua senha para confirmar.",
|
"delete_account_confirm": "Aviso: isso vai apagar seus dados de forma permanente. Escreva sua senha para confirmar.",
|
||||||
|
"@delete_account_confirm": {},
|
||||||
"new_password": "Nova senha",
|
"new_password": "Nova senha",
|
||||||
|
"@new_password": {},
|
||||||
"verify_password": "Verifique a senha",
|
"verify_password": "Verifique a senha",
|
||||||
|
"@verify_password": {},
|
||||||
"old_password": "Senha antiga",
|
"old_password": "Senha antiga",
|
||||||
|
"@old_password": {},
|
||||||
"show_avatars": "Mostrar Avatares",
|
"show_avatars": "Mostrar Avatares",
|
||||||
|
"@show_avatars": {},
|
||||||
"search": "Busca",
|
"search": "Busca",
|
||||||
|
"@search": {},
|
||||||
"send_message": "Enviar mensagem",
|
"send_message": "Enviar mensagem",
|
||||||
|
"@send_message": {},
|
||||||
"top_day": "Melhor do dia",
|
"top_day": "Melhor do dia",
|
||||||
|
"@top_day": {},
|
||||||
"top_week": "Melhor da semana",
|
"top_week": "Melhor da semana",
|
||||||
|
"@top_week": {},
|
||||||
"top_month": "Melhor do mês",
|
"top_month": "Melhor do mês",
|
||||||
|
"@top_month": {},
|
||||||
"top_year": "Melhor do ano",
|
"top_year": "Melhor do ano",
|
||||||
|
"@top_year": {},
|
||||||
"most_comments": "Mais comentados",
|
"most_comments": "Mais comentados",
|
||||||
|
"@most_comments": {},
|
||||||
"new_comments": "Novos comentários",
|
"new_comments": "Novos comentários",
|
||||||
"active": "Ativo"
|
"@new_comments": {},
|
||||||
|
"active": "Ativo",
|
||||||
|
"@active": {},
|
||||||
|
"report_reason_required": "Motivo da denúncia é necessário.",
|
||||||
|
"@report_reason_required": {},
|
||||||
|
"couldnt_create_report": "Não foi possível criar a denúncia.",
|
||||||
|
"@couldnt_create_report": {},
|
||||||
|
"bot_account": "Conta de bot",
|
||||||
|
"@bot_account": {},
|
||||||
|
"report_too_long": "Denúncia longa demais.",
|
||||||
|
"@report_too_long": {},
|
||||||
|
"couldnt_resolve_report": "Não foi possível resolver a denúncia.",
|
||||||
|
"@couldnt_resolve_report": {},
|
||||||
|
"top_all": "Melhor de sempre",
|
||||||
|
"@top_all": {},
|
||||||
|
"show_bot_accounts": "Mostrar contas de bots",
|
||||||
|
"@show_bot_accounts": {},
|
||||||
|
"show_read_posts": "Mostrar publicações lidas",
|
||||||
|
"@show_read_posts": {}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,6 @@ platform :android do
|
||||||
desc "Deploy a new version to the Google Play"
|
desc "Deploy a new version to the Google Play"
|
||||||
lane :prod do
|
lane :prod do
|
||||||
upload_to_play_store(
|
upload_to_play_store(
|
||||||
track: "beta",
|
|
||||||
package_name: "com.krawieck.lemmur",
|
package_name: "com.krawieck.lemmur",
|
||||||
json_key: ENV["GOOGLE_SERVICE_ACCOUNT_KEY_PATH"],
|
json_key: ENV["GOOGLE_SERVICE_ACCOUNT_KEY_PATH"],
|
||||||
aab: ENV["ABB_PATH"]
|
aab: ENV["ABB_PATH"]
|
||||||
|
|
|
@ -1,37 +1,85 @@
|
||||||
PODS:
|
PODS:
|
||||||
|
- DKImagePickerController/Core (4.3.3):
|
||||||
|
- DKImagePickerController/ImageDataManager
|
||||||
|
- DKImagePickerController/Resource
|
||||||
|
- DKImagePickerController/ImageDataManager (4.3.3)
|
||||||
|
- DKImagePickerController/PhotoGallery (4.3.3):
|
||||||
|
- DKImagePickerController/Core
|
||||||
|
- DKPhotoGallery
|
||||||
|
- DKImagePickerController/Resource (4.3.3)
|
||||||
|
- DKPhotoGallery (0.0.17):
|
||||||
|
- DKPhotoGallery/Core (= 0.0.17)
|
||||||
|
- DKPhotoGallery/Model (= 0.0.17)
|
||||||
|
- DKPhotoGallery/Preview (= 0.0.17)
|
||||||
|
- DKPhotoGallery/Resource (= 0.0.17)
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Core (0.0.17):
|
||||||
|
- DKPhotoGallery/Model
|
||||||
|
- DKPhotoGallery/Preview
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Model (0.0.17):
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Preview (0.0.17):
|
||||||
|
- DKPhotoGallery/Model
|
||||||
|
- DKPhotoGallery/Resource
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Resource (0.0.17):
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- file_picker (0.0.1):
|
||||||
|
- DKImagePickerController/PhotoGallery
|
||||||
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_keyboard_visibility (0.0.1):
|
- flutter_keyboard_visibility (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- image_picker (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- SDWebImage (5.12.5):
|
||||||
|
- SDWebImage/Core (= 5.12.5)
|
||||||
|
- SDWebImage/Core (5.12.5)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_ios (0.0.1):
|
- shared_preferences_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- SwiftyGif (5.4.3)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
- image_picker (from `.symlinks/plugins/image_picker/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
trunk:
|
||||||
|
- DKImagePickerController
|
||||||
|
- DKPhotoGallery
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
file_picker:
|
||||||
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_keyboard_visibility:
|
flutter_keyboard_visibility:
|
||||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||||
image_picker:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
|
@ -44,15 +92,20 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
DKImagePickerController: 72fd378f244cef3d27288e0aebf217a4467e4012
|
||||||
|
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||||
|
file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95
|
||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||||
image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6
|
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
|
SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e
|
||||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||||
shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32
|
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||||
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
|
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||||
|
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||||
|
|
||||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||||
|
|
||||||
COCOAPODS: 1.11.2
|
COCOAPODS: 1.11.3
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
@ -42,7 +42,9 @@
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false />
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true />
|
||||||
|
|
||||||
<!-- Image picker -->
|
<!-- Image picker -->
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
@ -51,5 +53,7 @@
|
||||||
<string>For uploading images for posts/avatars</string>
|
<string>For uploading images for posts/avatars</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>For recording videos for posts</string>
|
<string>For recording videos for posts</string>
|
||||||
</dict>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -4,6 +4,5 @@ template-arb-file: intl_en.arb
|
||||||
output-localization-file: l10n.dart
|
output-localization-file: l10n.dart
|
||||||
preferred-supported-locales: [en]
|
preferred-supported-locales: [en]
|
||||||
output-class: L10n
|
output-class: L10n
|
||||||
untranslated-messages-file: assets/l10n/untranslated.json
|
|
||||||
synthetic-package: false
|
synthetic-package: false
|
||||||
nullable-getter: false
|
nullable-getter: false
|
||||||
|
|
|
@ -5,13 +5,10 @@ import 'util/hot_rank.dart';
|
||||||
enum CommentSortType {
|
enum CommentSortType {
|
||||||
hot,
|
hot,
|
||||||
top,
|
top,
|
||||||
// ignore: constant_identifier_names
|
|
||||||
new_,
|
new_,
|
||||||
old,
|
old,
|
||||||
chat,
|
chat;
|
||||||
}
|
|
||||||
|
|
||||||
extension on CommentSortType {
|
|
||||||
/// returns a compare function for sorting a CommentTree according
|
/// returns a compare function for sorting a CommentTree according
|
||||||
/// to the comment sort type
|
/// to the comment sort type
|
||||||
int Function(CommentTree a, CommentTree b) get sortFunction {
|
int Function(CommentTree a, CommentTree b) get sortFunction {
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
|
|
||||||
ImagePicker useImagePicker() => useMemoized(ImagePicker.new);
|
|
|
@ -123,6 +123,7 @@ abstract class L10nStrings {
|
||||||
static const number_of_posts = 'number_of_posts';
|
static const number_of_posts = 'number_of_posts';
|
||||||
static const number_of_subscribers = 'number_of_subscribers';
|
static const number_of_subscribers = 'number_of_subscribers';
|
||||||
static const number_of_users = 'number_of_users';
|
static const number_of_users = 'number_of_users';
|
||||||
|
static const number_of_communities = 'number_of_communities';
|
||||||
static const unsubscribe = 'unsubscribe';
|
static const unsubscribe = 'unsubscribe';
|
||||||
static const subscribe = 'subscribe';
|
static const subscribe = 'subscribe';
|
||||||
static const messages = 'messages';
|
static const messages = 'messages';
|
||||||
|
@ -145,6 +146,24 @@ abstract class L10nStrings {
|
||||||
static const bot_account = 'bot_account';
|
static const bot_account = 'bot_account';
|
||||||
static const show_bot_accounts = 'show_bot_accounts';
|
static const show_bot_accounts = 'show_bot_accounts';
|
||||||
static const show_read_posts = 'show_read_posts';
|
static const show_read_posts = 'show_read_posts';
|
||||||
|
static const site_not_set_up = 'site_not_set_up';
|
||||||
|
static const nerd_stuff = 'nerd_stuff';
|
||||||
|
static const open_in_browser = 'open_in_browser';
|
||||||
|
static const cannot_open_in_browser = 'cannot_open_in_browser';
|
||||||
|
static const about = 'about';
|
||||||
|
static const see_all = 'see_all';
|
||||||
|
static const admins = 'admins';
|
||||||
|
static const trending_communities = 'trending_communities';
|
||||||
|
static const communities_of_instance = 'communities_of_instance';
|
||||||
|
static const day = 'day';
|
||||||
|
static const week = 'week';
|
||||||
|
static const month = 'month';
|
||||||
|
static const six_months = 'six_months';
|
||||||
|
static const add_instance = 'add_instance';
|
||||||
|
static const instance_added = 'instance_added';
|
||||||
|
static const required_field = 'required_field';
|
||||||
|
static const no_communities_found = 'no_communities_found';
|
||||||
|
static const network_error = 'network_error';
|
||||||
}
|
}
|
||||||
|
|
||||||
extension L10nFromString on String {
|
extension L10nFromString on String {
|
||||||
|
@ -406,6 +425,40 @@ extension L10nFromString on String {
|
||||||
return L10n.of(context).show_bot_accounts;
|
return L10n.of(context).show_bot_accounts;
|
||||||
case L10nStrings.show_read_posts:
|
case L10nStrings.show_read_posts:
|
||||||
return L10n.of(context).show_read_posts;
|
return L10n.of(context).show_read_posts;
|
||||||
|
case L10nStrings.site_not_set_up:
|
||||||
|
return L10n.of(context).site_not_set_up;
|
||||||
|
case L10nStrings.nerd_stuff:
|
||||||
|
return L10n.of(context).nerd_stuff;
|
||||||
|
case L10nStrings.open_in_browser:
|
||||||
|
return L10n.of(context).open_in_browser;
|
||||||
|
case L10nStrings.cannot_open_in_browser:
|
||||||
|
return L10n.of(context).cannot_open_in_browser;
|
||||||
|
case L10nStrings.about:
|
||||||
|
return L10n.of(context).about;
|
||||||
|
case L10nStrings.see_all:
|
||||||
|
return L10n.of(context).see_all;
|
||||||
|
case L10nStrings.admins:
|
||||||
|
return L10n.of(context).admins;
|
||||||
|
case L10nStrings.trending_communities:
|
||||||
|
return L10n.of(context).trending_communities;
|
||||||
|
case L10nStrings.day:
|
||||||
|
return L10n.of(context).day;
|
||||||
|
case L10nStrings.week:
|
||||||
|
return L10n.of(context).week;
|
||||||
|
case L10nStrings.month:
|
||||||
|
return L10n.of(context).month;
|
||||||
|
case L10nStrings.six_months:
|
||||||
|
return L10n.of(context).six_months;
|
||||||
|
case L10nStrings.add_instance:
|
||||||
|
return L10n.of(context).add_instance;
|
||||||
|
case L10nStrings.instance_added:
|
||||||
|
return L10n.of(context).instance_added;
|
||||||
|
case L10nStrings.required_field:
|
||||||
|
return L10n.of(context).required_field;
|
||||||
|
case L10nStrings.no_communities_found:
|
||||||
|
return L10n.of(context).no_communities_found;
|
||||||
|
case L10nStrings.network_error:
|
||||||
|
return L10n.of(context).network_error;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
const unorderedListTypes = ['*', '+', '-'];
|
||||||
|
const orderedListTypes = [')', '.'];
|
||||||
|
|
||||||
|
extension Utilities on String {
|
||||||
|
int getBeginningOfTheLine(int from) {
|
||||||
|
if (from <= 0) return 0;
|
||||||
|
for (var i = from; i >= 0; i--) {
|
||||||
|
if (this[i] == '\n') return i + 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getEndOfTheLine(int from) {
|
||||||
|
for (var i = from; i < length; i++) {
|
||||||
|
if (this[i] == '\n') return i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the line that ends at endingIndex
|
||||||
|
String lineUpTo(int characterIndex) {
|
||||||
|
return substring(getBeginningOfTheLine(characterIndex), characterIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on TextEditingValue {
|
||||||
|
/// Append a string after the cursor
|
||||||
|
TextEditingValue append(String s) {
|
||||||
|
final beg = text.substring(0, selection.baseOffset);
|
||||||
|
final end = text.substring(selection.baseOffset);
|
||||||
|
|
||||||
|
return copyWith(
|
||||||
|
text: '$beg$s$end',
|
||||||
|
selection: selection.copyWith(
|
||||||
|
baseOffset: selection.baseOffset + s.length,
|
||||||
|
extentOffset: selection.extentOffset + s.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// cuts [characterCount] number of chars from before the cursor
|
||||||
|
TextEditingValue trimBeforeCursor(int characterCount) {
|
||||||
|
final beg = text.substring(0, selection.baseOffset);
|
||||||
|
final end = text.substring(selection.baseOffset);
|
||||||
|
|
||||||
|
return copyWith(
|
||||||
|
text: beg.substring(0, beg.length - characterCount - 1) + end,
|
||||||
|
selection: selection.copyWith(
|
||||||
|
baseOffset: selection.baseOffset - characterCount,
|
||||||
|
extentOffset: selection.extentOffset - characterCount,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides convenience formatting in markdown text fields
|
||||||
|
class MarkdownFormatter extends TextInputFormatter {
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue, TextEditingValue newValue) {
|
||||||
|
if (oldValue.text.length > newValue.text.length) return newValue;
|
||||||
|
|
||||||
|
var newVal = newValue;
|
||||||
|
|
||||||
|
final char = newValue.text[newValue.selection.baseOffset - 1];
|
||||||
|
|
||||||
|
if (char == '\n') {
|
||||||
|
final lineBefore =
|
||||||
|
newValue.text.lineUpTo(newValue.selection.baseOffset - 2);
|
||||||
|
|
||||||
|
TextEditingValue unorderedListContinuation(
|
||||||
|
String listChar, TextEditingValue tev) {
|
||||||
|
final regex = RegExp(r'(\s*)' '${RegExp.escape(listChar)} (.*)');
|
||||||
|
final match = regex.matchAsPrefix(lineBefore);
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
return tev;
|
||||||
|
}
|
||||||
|
|
||||||
|
final listItemBody = match.group(2);
|
||||||
|
final indent = match.group(1);
|
||||||
|
|
||||||
|
if (listItemBody == null || listItemBody.isEmpty) {
|
||||||
|
return tev.trimBeforeCursor(listChar.length + (indent?.length ?? 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tev.append('$indent$listChar ');
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEditingValue orderedListContinuation(
|
||||||
|
String afterNumberChar, TextEditingValue tev) {
|
||||||
|
final regex =
|
||||||
|
RegExp(r'(\s*)(\d+)' '${RegExp.escape(afterNumberChar)} (.*)');
|
||||||
|
final match = regex.matchAsPrefix(lineBefore);
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
return tev;
|
||||||
|
}
|
||||||
|
|
||||||
|
final listItemBody = match.group(3)!;
|
||||||
|
final indent = match.group(1)!;
|
||||||
|
final numberStr = match.group(2)!;
|
||||||
|
|
||||||
|
if (listItemBody.isEmpty) {
|
||||||
|
return tev.trimBeforeCursor(
|
||||||
|
indent.length + numberStr.length + afterNumberChar.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final number = (int.tryParse(match.group(2)!) ?? 0) + 1;
|
||||||
|
return tev.append('$indent$number$afterNumberChar ');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final c in unorderedListTypes) {
|
||||||
|
newVal = unorderedListContinuation(c, newVal);
|
||||||
|
}
|
||||||
|
for (final c in orderedListTypes) {
|
||||||
|
newVal = orderedListContinuation(c, newVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newVal;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,18 +9,18 @@ import '../widgets/sortable_infinite_list.dart';
|
||||||
/// Infinite list of Communities fetched by the given fetcher
|
/// Infinite list of Communities fetched by the given fetcher
|
||||||
class CommunitiesListPage extends StatelessWidget {
|
class CommunitiesListPage extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Future<List<CommunityView>> Function(
|
final FetcherWithSorting<CommunityView> fetcher;
|
||||||
int page,
|
|
||||||
int batchSize,
|
|
||||||
SortType sortType,
|
|
||||||
) fetcher;
|
|
||||||
|
|
||||||
const CommunitiesListPage({Key? key, required this.fetcher, this.title = ''})
|
const CommunitiesListPage({
|
||||||
: super(key: key);
|
super.key,
|
||||||
|
required this.fetcher,
|
||||||
|
this.title = '',
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: theme.cardColor,
|
backgroundColor: theme.cardColor,
|
||||||
|
@ -45,8 +45,7 @@ class CommunitiesListPage extends StatelessWidget {
|
||||||
class CommunitiesListItem extends StatelessWidget {
|
class CommunitiesListItem extends StatelessWidget {
|
||||||
final CommunityView community;
|
final CommunityView community;
|
||||||
|
|
||||||
const CommunitiesListItem({Key? key, required this.community})
|
const CommunitiesListItem({super.key, required this.community});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ListTile(
|
Widget build(BuildContext context) => ListTile(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fuzzy/fuzzy.dart';
|
import 'package:fuzzy/fuzzy.dart';
|
||||||
|
@ -15,6 +16,7 @@ import '../util/goto.dart';
|
||||||
import '../util/text_color.dart';
|
import '../util/text_color.dart';
|
||||||
import '../widgets/avatar.dart';
|
import '../widgets/avatar.dart';
|
||||||
import '../widgets/pull_to_refresh.dart';
|
import '../widgets/pull_to_refresh.dart';
|
||||||
|
import 'instance/instance.dart';
|
||||||
|
|
||||||
/// List of subscribed communities per instance
|
/// List of subscribed communities per instance
|
||||||
class CommunitiesTab extends HookWidget {
|
class CommunitiesTab extends HookWidget {
|
||||||
|
@ -59,15 +61,14 @@ class CommunitiesTab extends HookWidget {
|
||||||
return Future.wait(futures);
|
return Future.wait(futures);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _loggedInAccounts = accountsStore.loggedInInstances
|
final loggedInAccounts = accountsStore.loggedInInstances
|
||||||
.map((instanceHost) =>
|
.map((instanceHost) =>
|
||||||
'$instanceHost${accountsStore.defaultUsernameFor(instanceHost)}')
|
'$instanceHost${accountsStore.defaultUsernameFor(instanceHost)}')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final instancesRefreshable =
|
final instancesRefreshable = useRefreshable(getInstances, loggedInAccounts);
|
||||||
useRefreshable(getInstances, _loggedInAccounts);
|
|
||||||
final communitiesRefreshable =
|
final communitiesRefreshable =
|
||||||
useRefreshable(getCommunities, _loggedInAccounts);
|
useRefreshable(getCommunities, loggedInAccounts);
|
||||||
|
|
||||||
if (communitiesRefreshable.snapshot.hasError ||
|
if (communitiesRefreshable.snapshot.hasError ||
|
||||||
instancesRefreshable.snapshot.hasError) {
|
instancesRefreshable.snapshot.hasError) {
|
||||||
|
@ -112,8 +113,11 @@ class CommunitiesTab extends HookWidget {
|
||||||
|
|
||||||
final instances = instancesRefreshable.snapshot.data!;
|
final instances = instancesRefreshable.snapshot.data!;
|
||||||
final communities = communitiesRefreshable.snapshot.data!
|
final communities = communitiesRefreshable.snapshot.data!
|
||||||
..forEach((e) =>
|
.map(
|
||||||
e.sort((a, b) => a.community.name.compareTo(b.community.name)));
|
(e) =>
|
||||||
|
e.sorted((a, b) => a.community.name.compareTo(b.community.name)),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final filterIcon = () {
|
final filterIcon = () {
|
||||||
if (filterController.text.isEmpty) {
|
if (filterController.text.isEmpty) {
|
||||||
|
@ -171,8 +175,11 @@ class CommunitiesTab extends HookWidget {
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => goToInstance(context,
|
onTap: () => Navigator.of(context).push(
|
||||||
accountsStore.loggedInInstances.elementAt(i)),
|
InstancePage.route(
|
||||||
|
accountsStore.loggedInInstances.elementAt(i),
|
||||||
|
),
|
||||||
|
),
|
||||||
onLongPress: () => toggleCollapse(i),
|
onLongPress: () => toggleCollapse(i),
|
||||||
leading: Avatar(
|
leading: Avatar(
|
||||||
url: instances[i].icon,
|
url: instances[i].icon,
|
||||||
|
@ -234,9 +241,11 @@ class _CommunitySubscribeToggle extends HookWidget {
|
||||||
final int communityId;
|
final int communityId;
|
||||||
final String instanceHost;
|
final String instanceHost;
|
||||||
|
|
||||||
const _CommunitySubscribeToggle(
|
const _CommunitySubscribeToggle({
|
||||||
{required this.instanceHost, required this.communityId, Key? key})
|
required this.instanceHost,
|
||||||
: super(key: key);
|
required this.communityId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import '../../util/share.dart';
|
||||||
import '../../widgets/failed_to_load.dart';
|
import '../../widgets/failed_to_load.dart';
|
||||||
import '../../widgets/reveal_after_scroll.dart';
|
import '../../widgets/reveal_after_scroll.dart';
|
||||||
import '../../widgets/sortable_infinite_list.dart';
|
import '../../widgets/sortable_infinite_list.dart';
|
||||||
import '../create_post.dart';
|
import '../create_post/create_post_fab.dart';
|
||||||
import 'community_about_tab.dart';
|
import 'community_about_tab.dart';
|
||||||
import 'community_more_menu.dart';
|
import 'community_more_menu.dart';
|
||||||
import 'community_overview.dart';
|
import 'community_overview.dart';
|
||||||
|
|
|
@ -15,8 +15,7 @@ import 'community_store.dart';
|
||||||
class CommmunityAboutTab extends StatelessWidget {
|
class CommmunityAboutTab extends StatelessWidget {
|
||||||
final FullCommunityView fullCommunityView;
|
final FullCommunityView fullCommunityView;
|
||||||
|
|
||||||
const CommmunityAboutTab(this.fullCommunityView, {Key? key})
|
const CommmunityAboutTab(this.fullCommunityView, {super.key});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
|
||||||
|
|
||||||
import '../../hooks/logged_in_action.dart';
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../url_launcher.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/mobx_provider.dart';
|
import '../../util/mobx_provider.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
|
@ -14,8 +14,7 @@ import 'community_store.dart';
|
||||||
class CommunityMoreMenu extends HookWidget {
|
class CommunityMoreMenu extends HookWidget {
|
||||||
final FullCommunityView fullCommunityView;
|
final FullCommunityView fullCommunityView;
|
||||||
|
|
||||||
const CommunityMoreMenu({Key? key, required this.fullCommunityView})
|
const CommunityMoreMenu({super.key, required this.fullCommunityView});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -28,10 +27,10 @@ class CommunityMoreMenu extends HookWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.open_in_browser),
|
leading: const Icon(Icons.open_in_browser),
|
||||||
title: const Text('Open in browser'),
|
title: const Text('Open in browser'),
|
||||||
onTap: () async => await ul.canLaunch(communityView.community.actorId)
|
onTap: () => launchLink(
|
||||||
? ul.launch(communityView.community.actorId)
|
link: communityView.community.actorId,
|
||||||
: ScaffoldMessenger.of(context).showSnackBar(
|
context: context,
|
||||||
const SnackBar(content: Text("can't open in browser"))),
|
),
|
||||||
),
|
),
|
||||||
ObserverBuilder<CommunityStore>(builder: (context, store) {
|
ObserverBuilder<CommunityStore>(builder: (context, store) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
|
|
@ -4,10 +4,10 @@ import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/goto.dart';
|
|
||||||
import '../../widgets/avatar.dart';
|
import '../../widgets/avatar.dart';
|
||||||
import '../../widgets/cached_network_image.dart';
|
import '../../widgets/cached_network_image.dart';
|
||||||
import '../../widgets/fullscreenable_image.dart';
|
import '../../widgets/fullscreenable_image.dart';
|
||||||
|
import '../instance/instance.dart';
|
||||||
import 'community_follow_button.dart';
|
import 'community_follow_button.dart';
|
||||||
|
|
||||||
class CommunityOverview extends StatelessWidget {
|
class CommunityOverview extends StatelessWidget {
|
||||||
|
@ -93,9 +93,10 @@ class CommunityOverview extends StatelessWidget {
|
||||||
text: community.community.originInstanceHost,
|
text: community.community.originInstanceHost,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () => goToInstance(
|
..onTap = () => Navigator.of(context).push(
|
||||||
context,
|
InstancePage.route(
|
||||||
community.community.originInstanceHost,
|
community.community.originInstanceHost,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -6,17 +6,19 @@ part of 'community_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$CommunityStore on _CommunityStore, Store {
|
mixin _$CommunityStore on _CommunityStore, Store {
|
||||||
final _$refreshAsyncAction = AsyncAction('_CommunityStore.refresh');
|
late final _$refreshAsyncAction =
|
||||||
|
AsyncAction('_CommunityStore.refresh', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> refresh(Jwt? token) {
|
Future<void> refresh(Jwt? token) {
|
||||||
return _$refreshAsyncAction.run(() => super.refresh(token));
|
return _$refreshAsyncAction.run(() => super.refresh(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$subscribeAsyncAction = AsyncAction('_CommunityStore.subscribe');
|
late final _$subscribeAsyncAction =
|
||||||
|
AsyncAction('_CommunityStore.subscribe', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> subscribe(Jwt token) {
|
Future<void> subscribe(Jwt token) {
|
||||||
|
|
|
@ -1,366 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:lemmy_api_client/pictrs.dart';
|
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
|
||||||
import '../hooks/image_picker.dart';
|
|
||||||
import '../hooks/logged_in_action.dart';
|
|
||||||
import '../hooks/memo_future.dart';
|
|
||||||
import '../hooks/stores.dart';
|
|
||||||
import '../l10n/l10n.dart';
|
|
||||||
import '../util/extensions/api.dart';
|
|
||||||
import '../util/extensions/spaced.dart';
|
|
||||||
import '../util/pictrs.dart';
|
|
||||||
import '../widgets/editor.dart';
|
|
||||||
import '../widgets/markdown_mode_icon.dart';
|
|
||||||
import '../widgets/radio_picker.dart';
|
|
||||||
import 'full_post/full_post.dart';
|
|
||||||
|
|
||||||
/// Fab that triggers the [CreatePost] modal
|
|
||||||
/// After creation it will navigate to the newly created post
|
|
||||||
class CreatePostFab extends HookWidget {
|
|
||||||
final CommunityView? community;
|
|
||||||
|
|
||||||
const CreatePostFab({this.community});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loggedInAction = useAnyLoggedInAction();
|
|
||||||
|
|
||||||
return FloatingActionButton(
|
|
||||||
onPressed: loggedInAction((_) async {
|
|
||||||
final postView = await Navigator.of(context).push(
|
|
||||||
community == null
|
|
||||||
? CreatePostPage.route()
|
|
||||||
: CreatePostPage.toCommunityRoute(community!),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (postView != null) {
|
|
||||||
await Navigator.of(context)
|
|
||||||
.push(FullPostPage.fromPostViewRoute(postView));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modal for creating a post to some community in some instance
|
|
||||||
/// Pops the navigator stack with a [PostView]
|
|
||||||
class CreatePostPage extends HookWidget {
|
|
||||||
final CommunityView? community;
|
|
||||||
|
|
||||||
final bool _isEdit;
|
|
||||||
final Post? post;
|
|
||||||
|
|
||||||
const CreatePostPage()
|
|
||||||
: community = null,
|
|
||||||
_isEdit = false,
|
|
||||||
post = null;
|
|
||||||
const CreatePostPage.toCommunity(CommunityView this.community)
|
|
||||||
: _isEdit = false,
|
|
||||||
post = null;
|
|
||||||
const CreatePostPage.edit(Post this.post)
|
|
||||||
: _isEdit = true,
|
|
||||||
community = null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final urlController =
|
|
||||||
useTextEditingController(text: _isEdit ? post?.url : null);
|
|
||||||
final titleController =
|
|
||||||
useTextEditingController(text: _isEdit ? post?.name : null);
|
|
||||||
final bodyController =
|
|
||||||
useTextEditingController(text: _isEdit ? post?.body : null);
|
|
||||||
final accStore = useAccountsStore();
|
|
||||||
final selectedInstance = useState(_isEdit
|
|
||||||
? post!.instanceHost
|
|
||||||
: community?.instanceHost ?? accStore.loggedInInstances.first);
|
|
||||||
final selectedCommunity = useState(community);
|
|
||||||
final showFancy = useState(false);
|
|
||||||
final nsfw = useState(_isEdit && post!.nsfw);
|
|
||||||
final delayed = useDelayedLoading();
|
|
||||||
final imagePicker = useImagePicker();
|
|
||||||
final imageUploadLoading = useState(false);
|
|
||||||
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
|
|
||||||
final loggedInAction = useLoggedInAction(selectedInstance.value);
|
|
||||||
|
|
||||||
final titleFocusNode = useFocusNode();
|
|
||||||
final bodyFocusNode = useFocusNode();
|
|
||||||
|
|
||||||
final allCommunitiesSnap = useMemoFuture(
|
|
||||||
() => LemmyApiV3(selectedInstance.value)
|
|
||||||
.run(ListCommunities(
|
|
||||||
type: PostListingType.all,
|
|
||||||
sort: SortType.hot,
|
|
||||||
limit: 9999,
|
|
||||||
auth: accStore.defaultUserDataFor(selectedInstance.value)?.jwt.raw,
|
|
||||||
))
|
|
||||||
.then(
|
|
||||||
(value) {
|
|
||||||
value.sort((a, b) => a.community.name.compareTo(b.community.name));
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
[selectedInstance.value],
|
|
||||||
);
|
|
||||||
|
|
||||||
uploadPicture(Jwt token) async {
|
|
||||||
try {
|
|
||||||
final pic = await imagePicker.pickImage(source: ImageSource.gallery);
|
|
||||||
// pic is null when the picker was cancelled
|
|
||||||
if (pic != null) {
|
|
||||||
imageUploadLoading.value = true;
|
|
||||||
|
|
||||||
final pictrs = PictrsApi(selectedInstance.value);
|
|
||||||
final upload =
|
|
||||||
await pictrs.upload(filePath: pic.path, auth: token.raw);
|
|
||||||
pictrsDeleteToken.value = upload.files[0];
|
|
||||||
urlController.text =
|
|
||||||
pathToPictrs(selectedInstance.value, upload.files[0].file);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Failed to upload image')));
|
|
||||||
} finally {
|
|
||||||
imageUploadLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removePicture(PictrsUploadFile deleteToken) {
|
|
||||||
PictrsApi(selectedInstance.value).delete(deleteToken).catchError((_) {});
|
|
||||||
|
|
||||||
pictrsDeleteToken.value = null;
|
|
||||||
urlController.text = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
final instanceDropdown = RadioPicker<String>(
|
|
||||||
values: accStore.loggedInInstances.toList(),
|
|
||||||
groupValue: selectedInstance.value,
|
|
||||||
onChanged: _isEdit ? null : (value) => selectedInstance.value = value,
|
|
||||||
buttonBuilder: (context, displayValue, onPressed) => TextButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(displayValue),
|
|
||||||
const Icon(Icons.arrow_drop_down),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
DropdownMenuItem<int> communityDropDownItem(CommunityView e) =>
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: e.community.id,
|
|
||||||
child: Text(e.community.local
|
|
||||||
? e.community.name
|
|
||||||
: '${e.community.originInstanceHost}/${e.community.name}'),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<DropdownMenuItem<int>> communitiesList() {
|
|
||||||
if (allCommunitiesSnap.hasData) {
|
|
||||||
return allCommunitiesSnap.data!.map(communityDropDownItem).toList();
|
|
||||||
} else {
|
|
||||||
if (selectedCommunity.value != null) {
|
|
||||||
return [communityDropDownItem(selectedCommunity.value!)];
|
|
||||||
} else {
|
|
||||||
return const [
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: -1,
|
|
||||||
child: CircularProgressIndicator.adaptive(),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(Jwt token) async {
|
|
||||||
if ((!_isEdit && selectedCommunity.value == null) ||
|
|
||||||
titleController.text.isEmpty) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
|
||||||
content: Text('Choosing a community and a title is required'),
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final api = LemmyApiV3(selectedInstance.value);
|
|
||||||
|
|
||||||
delayed.start();
|
|
||||||
try {
|
|
||||||
final res = await () {
|
|
||||||
if (_isEdit) {
|
|
||||||
return api.run(EditPost(
|
|
||||||
url: urlController.text.isEmpty ? null : urlController.text,
|
|
||||||
body: bodyController.text.isEmpty ? null : bodyController.text,
|
|
||||||
nsfw: nsfw.value,
|
|
||||||
name: titleController.text,
|
|
||||||
postId: post!.id,
|
|
||||||
auth: token.raw,
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
return api.run(CreatePost(
|
|
||||||
url: urlController.text.isEmpty ? null : urlController.text,
|
|
||||||
body: bodyController.text.isEmpty ? null : bodyController.text,
|
|
||||||
nsfw: nsfw.value,
|
|
||||||
name: titleController.text,
|
|
||||||
communityId: selectedCommunity.value!.community.id,
|
|
||||||
auth: token.raw,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
Navigator.of(context).pop(res);
|
|
||||||
return;
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(const SnackBar(content: Text('Failed to post')));
|
|
||||||
}
|
|
||||||
delayed.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: use lazy autocomplete
|
|
||||||
final communitiesDropdown = InputDecorator(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<int>(
|
|
||||||
value: selectedCommunity.value?.community.id,
|
|
||||||
hint: Text(L10n.of(context).community),
|
|
||||||
onChanged: _isEdit
|
|
||||||
? null
|
|
||||||
: (communityId) {
|
|
||||||
selectedCommunity.value = allCommunitiesSnap.data
|
|
||||||
?.firstWhere((e) => e.community.id == communityId);
|
|
||||||
},
|
|
||||||
items: communitiesList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final enabledUrlField = pictrsDeleteToken.value == null;
|
|
||||||
|
|
||||||
final url = Row(children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
enabled: enabledUrlField,
|
|
||||||
controller: urlController,
|
|
||||||
autofillHints: enabledUrlField ? const [AutofillHints.url] : null,
|
|
||||||
keyboardType: TextInputType.url,
|
|
||||||
onSubmitted: (_) => titleFocusNode.requestFocus(),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: L10n.of(context).url,
|
|
||||||
suffixIcon: const Icon(Icons.link),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
IconButton(
|
|
||||||
icon: imageUploadLoading.value
|
|
||||||
? const CircularProgressIndicator.adaptive()
|
|
||||||
: Icon(pictrsDeleteToken.value == null
|
|
||||||
? Icons.add_photo_alternate
|
|
||||||
: Icons.close),
|
|
||||||
onPressed: pictrsDeleteToken.value == null
|
|
||||||
? loggedInAction(uploadPicture)
|
|
||||||
: () => removePicture(pictrsDeleteToken.value!),
|
|
||||||
tooltip:
|
|
||||||
pictrsDeleteToken.value == null ? 'Add picture' : 'Delete picture',
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
|
|
||||||
final title = TextField(
|
|
||||||
controller: titleController,
|
|
||||||
focusNode: titleFocusNode,
|
|
||||||
keyboardType: TextInputType.text,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
onSubmitted: (_) => bodyFocusNode.requestFocus(),
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 2,
|
|
||||||
decoration: InputDecoration(labelText: L10n.of(context).title),
|
|
||||||
);
|
|
||||||
|
|
||||||
final body = Editor(
|
|
||||||
controller: bodyController,
|
|
||||||
focusNode: bodyFocusNode,
|
|
||||||
onSubmitted: (_) =>
|
|
||||||
delayed.pending ? () {} : loggedInAction(handleSubmit),
|
|
||||||
labelText: L10n.of(context).body,
|
|
||||||
instanceHost: selectedInstance.value,
|
|
||||||
fancy: showFancy.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: const CloseButton(),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: markdownModeIcon(fancy: showFancy.value),
|
|
||||||
onPressed: () => showFancy.value = !showFancy.value,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: ListView(
|
|
||||||
padding: const EdgeInsets.all(5),
|
|
||||||
children: [
|
|
||||||
instanceDropdown,
|
|
||||||
if (!_isEdit) communitiesDropdown,
|
|
||||||
url,
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => nsfw.value = !nsfw.value,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
value: nsfw.value,
|
|
||||||
onChanged: (val) {
|
|
||||||
if (val != null) nsfw.value = val;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(L10n.of(context).nsfw)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
delayed.pending ? () {} : loggedInAction(handleSubmit),
|
|
||||||
child: delayed.loading
|
|
||||||
? const CircularProgressIndicator.adaptive()
|
|
||||||
: Text(_isEdit
|
|
||||||
? L10n.of(context).edit
|
|
||||||
: L10n.of(context).post),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
].spaced(6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Route<PostView> route() => MaterialPageRoute(
|
|
||||||
builder: (context) => const CreatePostPage(),
|
|
||||||
fullscreenDialog: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
static Route<PostView> toCommunityRoute(CommunityView community) =>
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => CreatePostPage.toCommunity(community),
|
|
||||||
fullscreenDialog: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
static Route<PostView> editRoute(Post post) => MaterialPageRoute(
|
|
||||||
builder: (context) => CreatePostPage.edit(post),
|
|
||||||
fullscreenDialog: true,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:wc_form_validators/wc_form_validators.dart';
|
||||||
|
|
||||||
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../hooks/stores.dart';
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import '../../util/async_store_listener.dart';
|
||||||
|
import '../../util/extensions/spaced.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../widgets/editor/editor.dart';
|
||||||
|
import '../../widgets/markdown_mode_icon.dart';
|
||||||
|
import 'create_post_community_picker.dart';
|
||||||
|
import 'create_post_instance_picker.dart';
|
||||||
|
import 'create_post_store.dart';
|
||||||
|
import 'create_post_url_field.dart';
|
||||||
|
|
||||||
|
/// Modal for creating a post to some community in some instance
|
||||||
|
/// Pops the navigator stack with a [PostView]
|
||||||
|
class CreatePostPage extends HookWidget {
|
||||||
|
const CreatePostPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final formKey = useMemoized(GlobalKey<FormState>.new);
|
||||||
|
final loggedInAction = useLoggedInAction(
|
||||||
|
useStore((CreatePostStore store) => store.instanceHost),
|
||||||
|
);
|
||||||
|
|
||||||
|
final editorController = useEditorController(
|
||||||
|
instanceHost: context.read<CreatePostStore>().instanceHost,
|
||||||
|
text: context.read<CreatePostStore>().body);
|
||||||
|
final titleFocusNode = useFocusNode();
|
||||||
|
|
||||||
|
handleSubmit(Jwt token) async {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
await context.read<CreatePostStore>().submit(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final title = ObserverBuilder<CreatePostStore>(
|
||||||
|
builder: (context, store) => TextFormField(
|
||||||
|
initialValue: store.title,
|
||||||
|
focusNode: titleFocusNode,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
validator: Validators.required(L10n.of(context).required_field),
|
||||||
|
onChanged: (title) => store.title = title,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 2,
|
||||||
|
decoration: InputDecoration(labelText: L10n.of(context).title),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final body = ObserverBuilder<CreatePostStore>(
|
||||||
|
builder: (context, store) => Editor(
|
||||||
|
controller: editorController,
|
||||||
|
onChanged: (body) => store.body = body,
|
||||||
|
labelText: L10n.of(context).body,
|
||||||
|
fancy: store.showFancy,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return AsyncStoreListener<PostView>(
|
||||||
|
asyncStore: context.read<CreatePostStore>().submitState,
|
||||||
|
onSuccess: (context, data) {
|
||||||
|
Navigator.of(context).pop(data);
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
actions: [
|
||||||
|
ObserverBuilder<CreatePostStore>(
|
||||||
|
builder: (context, store) => IconButton(
|
||||||
|
icon: markdownModeIcon(fancy: store.showFancy),
|
||||||
|
onPressed: () => store.showFancy = !store.showFancy,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
|
child: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (!context.read<CreatePostStore>().isEdit) ...const [
|
||||||
|
CreatePostInstancePicker(),
|
||||||
|
CreatePostCommunityPicker(),
|
||||||
|
],
|
||||||
|
CreatePostUrlField(titleFocusNode),
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ObserverBuilder<CreatePostStore>(
|
||||||
|
builder: (context, store) => GestureDetector(
|
||||||
|
onTap: () => store.nsfw = !store.nsfw,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: store.nsfw,
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) store.nsfw = val;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(L10n.of(context).nsfw)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ObserverBuilder<CreatePostStore>(
|
||||||
|
builder: (context, store) => TextButton(
|
||||||
|
onPressed: store.submitState.isLoading
|
||||||
|
? () {}
|
||||||
|
: loggedInAction(handleSubmit),
|
||||||
|
child: store.submitState.isLoading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: Text(
|
||||||
|
store.isEdit
|
||||||
|
? L10n.of(context).edit
|
||||||
|
: L10n.of(context).post,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
EditorToolbar.safeArea,
|
||||||
|
].spaced(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BottomSticky(
|
||||||
|
child: EditorToolbar(editorController),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Route<PostView> route() => MaterialPageRoute(
|
||||||
|
builder: (context) => MobxProvider(
|
||||||
|
create: (context) => CreatePostStore(
|
||||||
|
instanceHost: context.read<AccountsStore>().loggedInInstances.first,
|
||||||
|
),
|
||||||
|
child: const CreatePostPage(),
|
||||||
|
),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
static Route<PostView> toCommunityRoute(CommunityView community) =>
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MobxProvider(
|
||||||
|
create: (context) => CreatePostStore(
|
||||||
|
instanceHost: community.instanceHost,
|
||||||
|
selectedCommunity: community,
|
||||||
|
),
|
||||||
|
child: const CreatePostPage(),
|
||||||
|
),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
static Route<PostView> editRoute(Post post) => MaterialPageRoute(
|
||||||
|
builder: (context) => MobxProvider(
|
||||||
|
create: (context) => CreatePostStore(
|
||||||
|
instanceHost: post.instanceHost,
|
||||||
|
postToEdit: post,
|
||||||
|
),
|
||||||
|
child: const CreatePostPage(),
|
||||||
|
),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:wc_form_validators/wc_form_validators.dart';
|
||||||
|
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../util/async_store_listener.dart';
|
||||||
|
import '../../util/extensions/api.dart';
|
||||||
|
import '../../util/extensions/context.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../widgets/avatar.dart';
|
||||||
|
import 'create_post_store.dart';
|
||||||
|
|
||||||
|
class CreatePostCommunityPicker extends HookWidget {
|
||||||
|
const CreatePostCommunityPicker({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final store = context.read<CreatePostStore>();
|
||||||
|
final controller = useTextEditingController(
|
||||||
|
text: store.selectedCommunity != null
|
||||||
|
? _communityString(store.selectedCommunity!)
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
return AsyncStoreListener(
|
||||||
|
asyncStore: context.read<CreatePostStore>().searchCommunitiesState,
|
||||||
|
child: Focus(
|
||||||
|
onFocusChange: (hasFocus) {
|
||||||
|
if (!hasFocus && store.selectedCommunity == null) {
|
||||||
|
controller.text = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ObserverBuilder<CreatePostStore>(builder: (context, store) {
|
||||||
|
return TypeAheadFormField<CommunityView>(
|
||||||
|
textFieldConfiguration: TextFieldConfiguration(
|
||||||
|
controller: controller,
|
||||||
|
enabled: !store.isEdit,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: L10n.of(context).community,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 16,
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
|
suffixIcon:
|
||||||
|
store.selectedCommunity == null && controller.text.isEmpty
|
||||||
|
? const Icon(Icons.arrow_drop_down)
|
||||||
|
: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
store.selectedCommunity = null;
|
||||||
|
controller.clear();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (_) => store.selectedCommunity = null,
|
||||||
|
),
|
||||||
|
validator: Validators.required(L10n.of(context).required_field),
|
||||||
|
suggestionsCallback: (pattern) async {
|
||||||
|
final communities = await store.searchCommunities(
|
||||||
|
pattern,
|
||||||
|
context.defaultJwt(store.instanceHost),
|
||||||
|
);
|
||||||
|
|
||||||
|
return communities ?? [];
|
||||||
|
},
|
||||||
|
itemBuilder: (context, community) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Avatar(
|
||||||
|
url: community.community.icon,
|
||||||
|
radius: 20,
|
||||||
|
),
|
||||||
|
title: Text(_communityString(community)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuggestionSelected: (community) {
|
||||||
|
store.selectedCommunity = community;
|
||||||
|
controller.text = _communityString(community);
|
||||||
|
},
|
||||||
|
noItemsFoundBuilder: (context) => SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(
|
||||||
|
L10n.of(context).no_communities_found,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loadingBuilder: (context) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
debounceDuration: const Duration(milliseconds: 400),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _communityString(CommunityView communityView) {
|
||||||
|
if (communityView.community.local) {
|
||||||
|
return communityView.community.title;
|
||||||
|
} else {
|
||||||
|
return '${communityView.community.originInstanceHost}/${communityView.community.title}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../full_post/full_post.dart';
|
||||||
|
import 'create_post.dart';
|
||||||
|
|
||||||
|
/// Fab that triggers the [CreatePost] modal
|
||||||
|
/// After creation it will navigate to the newly created post
|
||||||
|
class CreatePostFab extends HookWidget {
|
||||||
|
final CommunityView? community;
|
||||||
|
|
||||||
|
const CreatePostFab({this.community});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final loggedInAction = useAnyLoggedInAction();
|
||||||
|
|
||||||
|
return FloatingActionButton(
|
||||||
|
onPressed: loggedInAction((_) async {
|
||||||
|
final postView = await Navigator.of(context).push(
|
||||||
|
community == null
|
||||||
|
? CreatePostPage.route()
|
||||||
|
: CreatePostPage.toCommunityRoute(community!),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (postView != null) {
|
||||||
|
await Navigator.of(context)
|
||||||
|
.push(FullPostPage.fromPostViewRoute(postView));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../widgets/radio_picker.dart';
|
||||||
|
import 'create_post_store.dart';
|
||||||
|
|
||||||
|
class CreatePostInstancePicker extends StatelessWidget {
|
||||||
|
const CreatePostInstancePicker({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final loggedInInstances =
|
||||||
|
context.watch<AccountsStore>().loggedInInstances.toList();
|
||||||
|
|
||||||
|
return ObserverBuilder<CreatePostStore>(
|
||||||
|
builder: (context, store) => RadioPicker<String>(
|
||||||
|
values: loggedInInstances,
|
||||||
|
groupValue: store.instanceHost,
|
||||||
|
onChanged: store.isEdit ? null : (value) => store.instanceHost = value,
|
||||||
|
buttonBuilder: (context, displayValue, onPressed) => TextButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(displayValue),
|
||||||
|
const Icon(Icons.arrow_drop_down),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
import 'package:lemmy_api_client/pictrs.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../util/async_store.dart';
|
||||||
|
import '../../util/pictrs.dart';
|
||||||
|
|
||||||
|
part 'create_post_store.g.dart';
|
||||||
|
|
||||||
|
class CreatePostStore = _CreatePostStore with _$CreatePostStore;
|
||||||
|
|
||||||
|
abstract class _CreatePostStore with Store {
|
||||||
|
final Post? postToEdit;
|
||||||
|
bool get isEdit => postToEdit != null;
|
||||||
|
|
||||||
|
_CreatePostStore({
|
||||||
|
required this.instanceHost,
|
||||||
|
this.postToEdit,
|
||||||
|
// ignore: unused_element
|
||||||
|
this.selectedCommunity,
|
||||||
|
}) : title = postToEdit?.name ?? '',
|
||||||
|
nsfw = postToEdit?.nsfw ?? false,
|
||||||
|
body = postToEdit?.body ?? '',
|
||||||
|
url = postToEdit?.url ?? '';
|
||||||
|
|
||||||
|
@observable
|
||||||
|
bool showFancy = false;
|
||||||
|
@observable
|
||||||
|
String instanceHost;
|
||||||
|
@observable
|
||||||
|
CommunityView? selectedCommunity;
|
||||||
|
@observable
|
||||||
|
String url;
|
||||||
|
@observable
|
||||||
|
String title;
|
||||||
|
@observable
|
||||||
|
String body;
|
||||||
|
@observable
|
||||||
|
bool nsfw;
|
||||||
|
|
||||||
|
final submitState = AsyncStore<PostView>();
|
||||||
|
final searchCommunitiesState = AsyncStore<List<CommunityView>>();
|
||||||
|
final imageUploadState = AsyncStore<PictrsUploadFile>();
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get hasUploadedImage => imageUploadState.map(
|
||||||
|
loading: () => false,
|
||||||
|
error: (_) => false,
|
||||||
|
data: (_) => true,
|
||||||
|
);
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<List<CommunityView>?> searchCommunities(
|
||||||
|
String searchTerm,
|
||||||
|
Jwt? token,
|
||||||
|
) {
|
||||||
|
if (searchTerm.isEmpty) {
|
||||||
|
return searchCommunitiesState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
ListCommunities(
|
||||||
|
type: PostListingType.all,
|
||||||
|
sort: SortType.topAll,
|
||||||
|
limit: 10,
|
||||||
|
auth: token?.raw,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return searchCommunitiesState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
SearchCommunities(
|
||||||
|
q: searchTerm,
|
||||||
|
sort: SortType.topAll,
|
||||||
|
listingType: PostListingType.all,
|
||||||
|
limit: 10,
|
||||||
|
auth: token?.raw,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> submit(Jwt token) async {
|
||||||
|
await submitState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
isEdit
|
||||||
|
? EditPost(
|
||||||
|
url: url.isEmpty ? null : url,
|
||||||
|
body: body.isEmpty ? null : body,
|
||||||
|
nsfw: nsfw,
|
||||||
|
name: title,
|
||||||
|
postId: postToEdit!.id,
|
||||||
|
auth: token.raw,
|
||||||
|
)
|
||||||
|
: CreatePost(
|
||||||
|
url: url.isEmpty ? null : url,
|
||||||
|
body: body.isEmpty ? null : body,
|
||||||
|
nsfw: nsfw,
|
||||||
|
name: title,
|
||||||
|
communityId: selectedCommunity!.community.id,
|
||||||
|
auth: token.raw,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> uploadImage(String filePath, Jwt token) async {
|
||||||
|
final instanceHost = this.instanceHost;
|
||||||
|
|
||||||
|
final upload = await imageUploadState.run(
|
||||||
|
() => PictrsApi(instanceHost)
|
||||||
|
.upload(
|
||||||
|
filePath: filePath,
|
||||||
|
auth: token.raw,
|
||||||
|
)
|
||||||
|
.then((value) => value.files.single),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (upload != null) {
|
||||||
|
url = pathToPictrs(instanceHost, upload.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void removeImage() {
|
||||||
|
final pictrsFile = imageUploadState.map<PictrsUploadFile?>(
|
||||||
|
data: (data) => data,
|
||||||
|
loading: () => null,
|
||||||
|
error: (_) => null,
|
||||||
|
);
|
||||||
|
if (pictrsFile == null) return;
|
||||||
|
|
||||||
|
PictrsApi(instanceHost).delete(pictrsFile).catchError((_) {});
|
||||||
|
|
||||||
|
imageUploadState.reset();
|
||||||
|
url = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchCommunities implements LemmyApiQuery<List<CommunityView>> {
|
||||||
|
final Search base;
|
||||||
|
|
||||||
|
SearchCommunities({
|
||||||
|
required String q,
|
||||||
|
PostListingType? listingType,
|
||||||
|
SortType? sort,
|
||||||
|
int? page,
|
||||||
|
int? limit,
|
||||||
|
String? auth,
|
||||||
|
}) : base = Search(
|
||||||
|
q: q,
|
||||||
|
type: SearchType.communities,
|
||||||
|
listingType: listingType,
|
||||||
|
sort: sort,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
auth: auth,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get path => base.path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HttpMethod get httpMethod => base.httpMethod;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CommunityView> responseFactory(Map<String, dynamic> json) =>
|
||||||
|
base.responseFactory(json).communities;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => base.toJson();
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'create_post_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
mixin _$CreatePostStore on _CreatePostStore, Store {
|
||||||
|
Computed<bool>? _$hasUploadedImageComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasUploadedImage => (_$hasUploadedImageComputed ??= Computed<bool>(
|
||||||
|
() => super.hasUploadedImage,
|
||||||
|
name: '_CreatePostStore.hasUploadedImage'))
|
||||||
|
.value;
|
||||||
|
|
||||||
|
late final _$showFancyAtom =
|
||||||
|
Atom(name: '_CreatePostStore.showFancy', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get showFancy {
|
||||||
|
_$showFancyAtom.reportRead();
|
||||||
|
return super.showFancy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set showFancy(bool value) {
|
||||||
|
_$showFancyAtom.reportWrite(value, super.showFancy, () {
|
||||||
|
super.showFancy = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$instanceHostAtom =
|
||||||
|
Atom(name: '_CreatePostStore.instanceHost', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get instanceHost {
|
||||||
|
_$instanceHostAtom.reportRead();
|
||||||
|
return super.instanceHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set instanceHost(String value) {
|
||||||
|
_$instanceHostAtom.reportWrite(value, super.instanceHost, () {
|
||||||
|
super.instanceHost = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$selectedCommunityAtom =
|
||||||
|
Atom(name: '_CreatePostStore.selectedCommunity', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
CommunityView? get selectedCommunity {
|
||||||
|
_$selectedCommunityAtom.reportRead();
|
||||||
|
return super.selectedCommunity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set selectedCommunity(CommunityView? value) {
|
||||||
|
_$selectedCommunityAtom.reportWrite(value, super.selectedCommunity, () {
|
||||||
|
super.selectedCommunity = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$urlAtom = Atom(name: '_CreatePostStore.url', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get url {
|
||||||
|
_$urlAtom.reportRead();
|
||||||
|
return super.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set url(String value) {
|
||||||
|
_$urlAtom.reportWrite(value, super.url, () {
|
||||||
|
super.url = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$titleAtom =
|
||||||
|
Atom(name: '_CreatePostStore.title', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title {
|
||||||
|
_$titleAtom.reportRead();
|
||||||
|
return super.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set title(String value) {
|
||||||
|
_$titleAtom.reportWrite(value, super.title, () {
|
||||||
|
super.title = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$bodyAtom = Atom(name: '_CreatePostStore.body', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get body {
|
||||||
|
_$bodyAtom.reportRead();
|
||||||
|
return super.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set body(String value) {
|
||||||
|
_$bodyAtom.reportWrite(value, super.body, () {
|
||||||
|
super.body = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$nsfwAtom = Atom(name: '_CreatePostStore.nsfw', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get nsfw {
|
||||||
|
_$nsfwAtom.reportRead();
|
||||||
|
return super.nsfw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set nsfw(bool value) {
|
||||||
|
_$nsfwAtom.reportWrite(value, super.nsfw, () {
|
||||||
|
super.nsfw = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$submitAsyncAction =
|
||||||
|
AsyncAction('_CreatePostStore.submit', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> submit(Jwt token) {
|
||||||
|
return _$submitAsyncAction.run(() => super.submit(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$uploadImageAsyncAction =
|
||||||
|
AsyncAction('_CreatePostStore.uploadImage', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> uploadImage(String filePath, Jwt token) {
|
||||||
|
return _$uploadImageAsyncAction
|
||||||
|
.run(() => super.uploadImage(filePath, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$_CreatePostStoreActionController =
|
||||||
|
ActionController(name: '_CreatePostStore', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<CommunityView>?> searchCommunities(
|
||||||
|
String searchTerm, Jwt? token) {
|
||||||
|
final _$actionInfo = _$_CreatePostStoreActionController.startAction(
|
||||||
|
name: '_CreatePostStore.searchCommunities');
|
||||||
|
try {
|
||||||
|
return super.searchCommunities(searchTerm, token);
|
||||||
|
} finally {
|
||||||
|
_$_CreatePostStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void removeImage() {
|
||||||
|
final _$actionInfo = _$_CreatePostStoreActionController.startAction(
|
||||||
|
name: '_CreatePostStore.removeImage');
|
||||||
|
try {
|
||||||
|
return super.removeImage();
|
||||||
|
} finally {
|
||||||
|
_$_CreatePostStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
showFancy: ${showFancy},
|
||||||
|
instanceHost: ${instanceHost},
|
||||||
|
selectedCommunity: ${selectedCommunity},
|
||||||
|
url: ${url},
|
||||||
|
title: ${title},
|
||||||
|
body: ${body},
|
||||||
|
nsfw: ${nsfw},
|
||||||
|
hasUploadedImage: ${hasUploadedImage}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../hooks/stores.dart';
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../util/files.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import 'create_post_store.dart';
|
||||||
|
|
||||||
|
class CreatePostUrlField extends HookWidget {
|
||||||
|
final FocusNode titleFocusNode;
|
||||||
|
|
||||||
|
const CreatePostUrlField(this.titleFocusNode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final controller = useTextEditingController(
|
||||||
|
text: context.read<CreatePostStore>().url,
|
||||||
|
);
|
||||||
|
final loggedInAction = useLoggedInAction(
|
||||||
|
useStore((CreatePostStore store) => store.instanceHost),
|
||||||
|
);
|
||||||
|
|
||||||
|
uploadImage(Jwt token) async {
|
||||||
|
final pic = await pickImage();
|
||||||
|
|
||||||
|
// pic is null when the picker was cancelled
|
||||||
|
if (pic != null) {
|
||||||
|
await context.read<CreatePostStore>().uploadImage(pic.path, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ObserverConsumer<CreatePostStore>(
|
||||||
|
listener: (context, store) {
|
||||||
|
// needed to keep the controller and store data in sync
|
||||||
|
if (controller.text != store.url) {
|
||||||
|
controller.text = store.url;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, store) => Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
enabled: !store.hasUploadedImage,
|
||||||
|
autofillHints:
|
||||||
|
!store.hasUploadedImage ? const [AutofillHints.url] : null,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onFieldSubmitted: (_) => titleFocusNode.requestFocus(),
|
||||||
|
onChanged: (url) => store.url = url,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: L10n.of(context).url,
|
||||||
|
suffixIcon: const Icon(Icons.link),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
IconButton(
|
||||||
|
icon: store.imageUploadState.isLoading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: Icon(
|
||||||
|
store.hasUploadedImage
|
||||||
|
? Icons.close
|
||||||
|
: Icons.add_photo_alternate,
|
||||||
|
),
|
||||||
|
onPressed: store.hasUploadedImage
|
||||||
|
? () => store.removeImage()
|
||||||
|
: loggedInAction(uploadImage),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ class CommentSection extends StatelessWidget {
|
||||||
CommentSortType.chat: _SortSelection(Icons.chat, L10nStrings.chat),
|
CommentSortType.chat: _SortSelection(Icons.chat, L10nStrings.chat),
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommentSection({Key? key}) : super(key: key);
|
const CommentSection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -14,7 +14,6 @@ abstract class _FullPostStore with Store {
|
||||||
final String instanceHost;
|
final String instanceHost;
|
||||||
|
|
||||||
_FullPostStore({
|
_FullPostStore({
|
||||||
this.postStore,
|
|
||||||
required this.postId,
|
required this.postId,
|
||||||
required this.instanceHost,
|
required this.instanceHost,
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ part of 'full_post_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$FullPostStore on _FullPostStore, Store {
|
mixin _$FullPostStore on _FullPostStore, Store {
|
||||||
Computed<List<CommentTree>?>? _$commentTreeComputed;
|
Computed<List<CommentTree>?>? _$commentTreeComputed;
|
||||||
|
@ -38,7 +38,8 @@ mixin _$FullPostStore on _FullPostStore, Store {
|
||||||
name: '_FullPostStore.comments'))
|
name: '_FullPostStore.comments'))
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
final _$fullPostViewAtom = Atom(name: '_FullPostStore.fullPostView');
|
late final _$fullPostViewAtom =
|
||||||
|
Atom(name: '_FullPostStore.fullPostView', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FullPostView? get fullPostView {
|
FullPostView? get fullPostView {
|
||||||
|
@ -53,7 +54,8 @@ mixin _$FullPostStore on _FullPostStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$newCommentsAtom = Atom(name: '_FullPostStore.newComments');
|
late final _$newCommentsAtom =
|
||||||
|
Atom(name: '_FullPostStore.newComments', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ObservableList<CommentView> get newComments {
|
ObservableList<CommentView> get newComments {
|
||||||
|
@ -68,7 +70,8 @@ mixin _$FullPostStore on _FullPostStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$sortingAtom = Atom(name: '_FullPostStore.sorting');
|
late final _$sortingAtom =
|
||||||
|
Atom(name: '_FullPostStore.sorting', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CommentSortType get sorting {
|
CommentSortType get sorting {
|
||||||
|
@ -83,7 +86,8 @@ mixin _$FullPostStore on _FullPostStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$postStoreAtom = Atom(name: '_FullPostStore.postStore');
|
late final _$postStoreAtom =
|
||||||
|
Atom(name: '_FullPostStore.postStore', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PostStore? get postStore {
|
PostStore? get postStore {
|
||||||
|
@ -98,23 +102,24 @@ mixin _$FullPostStore on _FullPostStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$refreshAsyncAction = AsyncAction('_FullPostStore.refresh');
|
late final _$refreshAsyncAction =
|
||||||
|
AsyncAction('_FullPostStore.refresh', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> refresh([Jwt? token]) {
|
Future<void> refresh([Jwt? token]) {
|
||||||
return _$refreshAsyncAction.run(() => super.refresh(token));
|
return _$refreshAsyncAction.run(() => super.refresh(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$blockCommunityAsyncAction =
|
late final _$blockCommunityAsyncAction =
|
||||||
AsyncAction('_FullPostStore.blockCommunity');
|
AsyncAction('_FullPostStore.blockCommunity', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> blockCommunity(Jwt token) {
|
Future<void> blockCommunity(Jwt token) {
|
||||||
return _$blockCommunityAsyncAction.run(() => super.blockCommunity(token));
|
return _$blockCommunityAsyncAction.run(() => super.blockCommunity(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_FullPostStoreActionController =
|
late final _$_FullPostStoreActionController =
|
||||||
ActionController(name: '_FullPostStore');
|
ActionController(name: '_FullPostStore', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updateSorting(CommentSortType sort) {
|
void updateSorting(CommentSortType sort) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
import '../util/extensions/brightness.dart';
|
import '../util/extensions/brightness.dart';
|
||||||
import 'communities_tab.dart';
|
import 'communities_tab.dart';
|
||||||
import 'create_post.dart';
|
import 'create_post/create_post_fab.dart';
|
||||||
import 'home_tab.dart';
|
import 'home_tab.dart';
|
||||||
import 'profile_tab.dart';
|
import 'profile_tab.dart';
|
||||||
import 'search_tab.dart';
|
import 'search_tab.dart';
|
||||||
|
|
|
@ -15,6 +15,7 @@ import '../widgets/cached_network_image.dart';
|
||||||
import '../widgets/infinite_scroll.dart';
|
import '../widgets/infinite_scroll.dart';
|
||||||
import '../widgets/sortable_infinite_list.dart';
|
import '../widgets/sortable_infinite_list.dart';
|
||||||
import 'inbox.dart';
|
import 'inbox.dart';
|
||||||
|
import 'instance/instance.dart';
|
||||||
import 'settings/add_account_page.dart';
|
import 'settings/add_account_page.dart';
|
||||||
|
|
||||||
/// First thing users sees when opening the app
|
/// First thing users sees when opening the app
|
||||||
|
@ -128,7 +129,9 @@ class HomeTab extends HookWidget {
|
||||||
color:
|
color:
|
||||||
theme.textTheme.bodyText1?.color?.withOpacity(0.7)),
|
theme.textTheme.bodyText1?.color?.withOpacity(0.7)),
|
||||||
),
|
),
|
||||||
onTap: () => goToInstance(context, instance),
|
onTap: () => Navigator.of(context).push(
|
||||||
|
InstancePage.route(instance),
|
||||||
|
),
|
||||||
dense: true,
|
dense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
|
|
|
@ -1,387 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
|
||||||
|
|
||||||
import '../hooks/stores.dart';
|
|
||||||
import '../l10n/l10n.dart';
|
|
||||||
import '../util/extensions/spaced.dart';
|
|
||||||
import '../util/goto.dart';
|
|
||||||
import '../util/icons.dart';
|
|
||||||
import '../util/share.dart';
|
|
||||||
import '../util/text_color.dart';
|
|
||||||
import '../widgets/avatar.dart';
|
|
||||||
import '../widgets/bottom_modal.dart';
|
|
||||||
import '../widgets/cached_network_image.dart';
|
|
||||||
import '../widgets/fullscreenable_image.dart';
|
|
||||||
import '../widgets/info_table_popup.dart';
|
|
||||||
import '../widgets/markdown_text.dart';
|
|
||||||
import '../widgets/reveal_after_scroll.dart';
|
|
||||||
import '../widgets/sortable_infinite_list.dart';
|
|
||||||
import '../widgets/user_tile.dart';
|
|
||||||
import 'communities_list.dart';
|
|
||||||
import 'modlog/modlog.dart';
|
|
||||||
|
|
||||||
/// Displays posts, comments, and general info about the given instance
|
|
||||||
class InstancePage extends HookWidget {
|
|
||||||
final String instanceHost;
|
|
||||||
final Future<FullSiteView> siteFuture;
|
|
||||||
final Future<List<CommunityView>> communitiesFuture;
|
|
||||||
|
|
||||||
InstancePage({required this.instanceHost})
|
|
||||||
: siteFuture = LemmyApiV3(instanceHost).run(const GetSite()),
|
|
||||||
communitiesFuture = LemmyApiV3(instanceHost).run(const ListCommunities(
|
|
||||||
type: PostListingType.local, sort: SortType.hot, limit: 6));
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final siteSnap = useFuture(siteFuture);
|
|
||||||
final colorOnCard = textColorBasedOnBackground(theme.cardColor);
|
|
||||||
final accStore = useAccountsStore();
|
|
||||||
final scrollController = useScrollController();
|
|
||||||
|
|
||||||
if (!siteSnap.hasData || siteSnap.data!.siteView == null) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(),
|
|
||||||
body: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (siteSnap.hasError) ...[
|
|
||||||
const Icon(Icons.error),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Text('ERROR: ${siteSnap.error}'),
|
|
||||||
)
|
|
||||||
] else if (siteSnap.hasData && siteSnap.data!.siteView == null)
|
|
||||||
const Text('ERROR')
|
|
||||||
else
|
|
||||||
const CircularProgressIndicator.adaptive(
|
|
||||||
semanticsLabel: 'loading')
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final site = siteSnap.data!;
|
|
||||||
final siteView = site.siteView!;
|
|
||||||
|
|
||||||
void _share() => share('https://$instanceHost', context: context);
|
|
||||||
|
|
||||||
void _openMoreMenu() {
|
|
||||||
showBottomModal(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => Column(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.open_in_browser),
|
|
||||||
title: const Text('Open in browser'),
|
|
||||||
onTap: () async => await ul
|
|
||||||
.canLaunch('https://${site.instanceHost}')
|
|
||||||
? ul.launch('https://${site.instanceHost}')
|
|
||||||
: ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text("can't open in browser"))),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.info_outline),
|
|
||||||
title: const Text('Nerd stuff'),
|
|
||||||
onTap: () {
|
|
||||||
showInfoTablePopup(context: context, table: site.toJson());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
body: DefaultTabController(
|
|
||||||
length: 3,
|
|
||||||
child: NestedScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
|
|
||||||
SliverAppBar(
|
|
||||||
expandedHeight: 250,
|
|
||||||
pinned: true,
|
|
||||||
backgroundColor: theme.cardColor,
|
|
||||||
title: RevealAfterScroll(
|
|
||||||
after: 150,
|
|
||||||
fade: true,
|
|
||||||
scrollController: scrollController,
|
|
||||||
child: Text(
|
|
||||||
siteView.site.name,
|
|
||||||
style: TextStyle(color: colorOnCard),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
IconButton(icon: Icon(shareIcon), onPressed: _share),
|
|
||||||
IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu),
|
|
||||||
],
|
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
|
||||||
background: Stack(children: [
|
|
||||||
if (siteView.site.banner != null)
|
|
||||||
FullscreenableImage(
|
|
||||||
url: siteView.site.banner!,
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: siteView.site.banner!,
|
|
||||||
errorBuilder: (_, ___) => const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SafeArea(
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 40),
|
|
||||||
child: siteView.site.icon == null
|
|
||||||
? const SizedBox(height: 100, width: 100)
|
|
||||||
: FullscreenableImage(
|
|
||||||
url: siteView.site.icon!,
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
imageUrl: siteView.site.icon!,
|
|
||||||
errorBuilder: (_, ___) =>
|
|
||||||
const Icon(Icons.warning),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(siteView.site.name,
|
|
||||||
style: theme.textTheme.headline6),
|
|
||||||
Text(instanceHost, style: theme.textTheme.caption)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
bottom: PreferredSize(
|
|
||||||
preferredSize: const TabBar(tabs: []).preferredSize,
|
|
||||||
child: Material(
|
|
||||||
color: theme.cardColor,
|
|
||||||
child: TabBar(
|
|
||||||
tabs: [
|
|
||||||
Tab(text: L10n.of(context).posts),
|
|
||||||
Tab(text: L10n.of(context).comments),
|
|
||||||
const Tab(text: 'About'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
body: TabBarView(
|
|
||||||
children: [
|
|
||||||
InfinitePostList(
|
|
||||||
fetcher: (page, batchSize, sort) =>
|
|
||||||
LemmyApiV3(instanceHost).run(GetPosts(
|
|
||||||
// TODO: switch between all and subscribed
|
|
||||||
type: PostListingType.all,
|
|
||||||
sort: sort,
|
|
||||||
limit: batchSize,
|
|
||||||
page: page,
|
|
||||||
savedOnly: false,
|
|
||||||
auth:
|
|
||||||
accStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
|
||||||
))),
|
|
||||||
InfiniteCommentList(
|
|
||||||
fetcher: (page, batchSize, sort) =>
|
|
||||||
LemmyApiV3(instanceHost).run(GetComments(
|
|
||||||
type: CommentListingType.all,
|
|
||||||
sort: sort,
|
|
||||||
limit: batchSize,
|
|
||||||
page: page,
|
|
||||||
savedOnly: false,
|
|
||||||
auth:
|
|
||||||
accStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
|
||||||
))),
|
|
||||||
_AboutTab(site,
|
|
||||||
communitiesFuture: communitiesFuture,
|
|
||||||
instanceHost: instanceHost),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AboutTab extends HookWidget {
|
|
||||||
final FullSiteView site;
|
|
||||||
final Future<List<CommunityView>> communitiesFuture;
|
|
||||||
final String instanceHost;
|
|
||||||
|
|
||||||
const _AboutTab(
|
|
||||||
this.site, {
|
|
||||||
required this.communitiesFuture,
|
|
||||||
required this.instanceHost,
|
|
||||||
});
|
|
||||||
|
|
||||||
// void goToBannedUsers(BuildContext context) {
|
|
||||||
// goTo(
|
|
||||||
// context,
|
|
||||||
// (_) => UsersListPage(
|
|
||||||
// users: site.banned.reversed.toList(),
|
|
||||||
// title: L10n.of(context).banned_users,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final commSnap = useFuture(communitiesFuture);
|
|
||||||
final accStore = useAccountsStore();
|
|
||||||
|
|
||||||
void goToCommunities() {
|
|
||||||
goTo(
|
|
||||||
context,
|
|
||||||
(_) => CommunitiesListPage(
|
|
||||||
fetcher: (page, batchSize, sortType) => LemmyApiV3(instanceHost).run(
|
|
||||||
ListCommunities(
|
|
||||||
type: PostListingType.local,
|
|
||||||
sort: sortType,
|
|
||||||
limit: batchSize,
|
|
||||||
page: page,
|
|
||||||
auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: 'Communities of ${site.siteView?.site.name}',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final siteView = site.siteView;
|
|
||||||
|
|
||||||
if (siteView == null) {
|
|
||||||
return const SingleChildScrollView(
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
child: Text('error'),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: SafeArea(
|
|
||||||
top: false,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (siteView.site.description != null) ...[
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
|
|
||||||
child: MarkdownText(
|
|
||||||
siteView.site.description!,
|
|
||||||
instanceHost: instanceHost,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const _Divider(),
|
|
||||||
],
|
|
||||||
SizedBox(
|
|
||||||
height: 32,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
||||||
children: [
|
|
||||||
Chip(
|
|
||||||
label: Text(L10n.of(context)
|
|
||||||
.number_of_users_online(site.online))),
|
|
||||||
Chip(
|
|
||||||
label: Text(
|
|
||||||
'${siteView.counts.usersActiveDay} users / day')),
|
|
||||||
Chip(
|
|
||||||
label: Text(
|
|
||||||
'${siteView.counts.usersActiveWeek} users / week')),
|
|
||||||
Chip(
|
|
||||||
label: Text(
|
|
||||||
'${siteView.counts.usersActiveMonth} users / month')),
|
|
||||||
Chip(
|
|
||||||
label: Text(
|
|
||||||
'${siteView.counts.usersActiveHalfYear} users / 6 months')),
|
|
||||||
Chip(
|
|
||||||
label: Text(L10n.of(context)
|
|
||||||
.number_of_users(siteView.counts.users))),
|
|
||||||
Chip(
|
|
||||||
label:
|
|
||||||
Text('${siteView.counts.communities} communities')),
|
|
||||||
Chip(label: Text('${siteView.counts.posts} posts')),
|
|
||||||
Chip(label: Text('${siteView.counts.comments} comments')),
|
|
||||||
].spaced(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const _Divider(),
|
|
||||||
ListTile(
|
|
||||||
title: Center(
|
|
||||||
child: Text(
|
|
||||||
'Trending communities:',
|
|
||||||
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (commSnap.hasData)
|
|
||||||
for (final c in commSnap.data!)
|
|
||||||
ListTile(
|
|
||||||
onTap: () => goToCommunity.byId(
|
|
||||||
context, c.instanceHost, c.community.id),
|
|
||||||
title: Text(c.community.name),
|
|
||||||
leading: Avatar(url: c.community.icon),
|
|
||||||
)
|
|
||||||
else if (commSnap.hasError)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Text("Can't load communities, ${commSnap.error}"),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 10),
|
|
||||||
child: CircularProgressIndicator.adaptive(),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: const Center(child: Text('See all')),
|
|
||||||
onTap: goToCommunities,
|
|
||||||
),
|
|
||||||
const _Divider(),
|
|
||||||
ListTile(
|
|
||||||
title: Center(
|
|
||||||
child: Text(
|
|
||||||
'Admins:',
|
|
||||||
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final u in site.admins)
|
|
||||||
PersonTile(
|
|
||||||
u.person,
|
|
||||||
expanded: true,
|
|
||||||
),
|
|
||||||
const _Divider(),
|
|
||||||
// TODO: transition to new API
|
|
||||||
// ListTile(
|
|
||||||
// title: Center(child: Text(L10n.of(context).banned_users)),
|
|
||||||
// onTap: () => goToBannedUsers(context),
|
|
||||||
// ),
|
|
||||||
ListTile(
|
|
||||||
title: Center(child: Text(L10n.of(context).modlog)),
|
|
||||||
onTap: () => Navigator.of(context).push(
|
|
||||||
ModlogPage.forInstanceRoute(instanceHost),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Divider extends StatelessWidget {
|
|
||||||
const _Divider();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
|
||||||
child: Divider(),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../util/extensions/context.dart';
|
||||||
|
import '../../util/icons.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../util/share.dart';
|
||||||
|
import '../../util/text_color.dart';
|
||||||
|
import '../../widgets/cached_network_image.dart';
|
||||||
|
import '../../widgets/failed_to_load.dart';
|
||||||
|
import '../../widgets/fullscreenable_image.dart';
|
||||||
|
import '../../widgets/reveal_after_scroll.dart';
|
||||||
|
import '../../widgets/sortable_infinite_list.dart';
|
||||||
|
import 'instance_about_tab.dart';
|
||||||
|
import 'instance_more_menu.dart';
|
||||||
|
import 'instance_store.dart';
|
||||||
|
|
||||||
|
/// Displays posts, comments, and general info about the given instance
|
||||||
|
class InstancePage extends HookWidget {
|
||||||
|
const InstancePage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorOnCard = textColorBasedOnBackground(theme.cardColor);
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
|
||||||
|
return ObserverBuilder<InstanceStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
final instanceUrl = 'https://${store.instanceHost}';
|
||||||
|
|
||||||
|
return store.siteState.map(
|
||||||
|
loading: () => Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: const Center(child: CircularProgressIndicator.adaptive()),
|
||||||
|
),
|
||||||
|
error: (errorTerm) => Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: Center(
|
||||||
|
child: FailedToLoad(
|
||||||
|
refresh: () => store.fetch(
|
||||||
|
context.defaultJwt(store.instanceHost),
|
||||||
|
),
|
||||||
|
message: errorTerm.tr(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: (site) {
|
||||||
|
final siteView = site.siteView;
|
||||||
|
|
||||||
|
if (siteView == null) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: Center(child: Text(L10n.of(context).site_not_set_up)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: DefaultTabController(
|
||||||
|
length: 3,
|
||||||
|
child: NestedScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
|
SliverAppBar(
|
||||||
|
expandedHeight: 250,
|
||||||
|
pinned: true,
|
||||||
|
backgroundColor: theme.cardColor,
|
||||||
|
title: RevealAfterScroll(
|
||||||
|
after: 150,
|
||||||
|
fade: true,
|
||||||
|
scrollController: scrollController,
|
||||||
|
child: Text(
|
||||||
|
siteView.site.name,
|
||||||
|
style: TextStyle(color: colorOnCard),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(shareIcon),
|
||||||
|
onPressed: () => share(instanceUrl, context: context),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(moreIcon),
|
||||||
|
onPressed: () => InstanceMoreMenu.open(context, site),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
background: Stack(
|
||||||
|
children: [
|
||||||
|
if (siteView.site.banner != null)
|
||||||
|
FullscreenableImage(
|
||||||
|
url: siteView.site.banner!,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: siteView.site.banner!,
|
||||||
|
errorBuilder: (_, ___) => const SizedBox(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 40),
|
||||||
|
child: siteView.site.icon == null
|
||||||
|
? const SizedBox(
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
)
|
||||||
|
: FullscreenableImage(
|
||||||
|
url: siteView.site.icon!,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
imageUrl: siteView.site.icon!,
|
||||||
|
errorBuilder: (_, ___) =>
|
||||||
|
const Icon(Icons.warning),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
siteView.site.name,
|
||||||
|
style: theme.textTheme.headline6,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
store.instanceHost,
|
||||||
|
style: theme.textTheme.caption,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const TabBar(tabs: []).preferredSize,
|
||||||
|
child: Material(
|
||||||
|
color: theme.cardColor,
|
||||||
|
child: TabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(text: L10n.of(context).posts),
|
||||||
|
Tab(text: L10n.of(context).comments),
|
||||||
|
Tab(text: L10n.of(context).about),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: TabBarView(
|
||||||
|
children: [
|
||||||
|
InfinitePostList(
|
||||||
|
fetcher: (page, batchSize, sort) =>
|
||||||
|
LemmyApiV3(store.instanceHost).run(GetPosts(
|
||||||
|
// TODO: switch between all and subscribed
|
||||||
|
type: PostListingType.all,
|
||||||
|
sort: sort,
|
||||||
|
limit: batchSize,
|
||||||
|
page: page,
|
||||||
|
savedOnly: false,
|
||||||
|
auth: context.defaultJwt(store.instanceHost)?.raw,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
InfiniteCommentList(
|
||||||
|
fetcher: (page, batchSize, sort) =>
|
||||||
|
LemmyApiV3(store.instanceHost).run(GetComments(
|
||||||
|
type: CommentListingType.all,
|
||||||
|
sort: sort,
|
||||||
|
limit: batchSize,
|
||||||
|
page: page,
|
||||||
|
savedOnly: false,
|
||||||
|
auth: context.defaultJwt(store.instanceHost)?.raw,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
InstanceAboutTab(
|
||||||
|
site: site,
|
||||||
|
siteView: siteView,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Route route(String instanceHost) {
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return MobxProvider(
|
||||||
|
create: (context) => InstanceStore(instanceHost)
|
||||||
|
..fetch(context.defaultJwt(instanceHost)),
|
||||||
|
child: const InstancePage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../util/extensions/context.dart';
|
||||||
|
import '../../util/extensions/spaced.dart';
|
||||||
|
import '../../util/goto.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../widgets/avatar.dart';
|
||||||
|
import '../../widgets/failed_to_load.dart';
|
||||||
|
import '../../widgets/markdown_text.dart';
|
||||||
|
import '../../widgets/pull_to_refresh.dart';
|
||||||
|
import '../../widgets/user_tile.dart';
|
||||||
|
import '../communities_list.dart';
|
||||||
|
import '../community/community.dart';
|
||||||
|
import '../modlog/modlog.dart';
|
||||||
|
import 'instance_store.dart';
|
||||||
|
|
||||||
|
class InstanceAboutTab extends HookWidget {
|
||||||
|
final FullSiteView site;
|
||||||
|
final SiteView siteView;
|
||||||
|
|
||||||
|
const InstanceAboutTab({required this.site, required this.siteView});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final l10n = L10n.of(context);
|
||||||
|
|
||||||
|
void goToCommunities() {
|
||||||
|
goTo(
|
||||||
|
context,
|
||||||
|
(_) => CommunitiesListPage(
|
||||||
|
fetcher: (page, batchSize, sortType) =>
|
||||||
|
LemmyApiV3(site.instanceHost).run(
|
||||||
|
ListCommunities(
|
||||||
|
type: PostListingType.local,
|
||||||
|
sort: sortType,
|
||||||
|
limit: batchSize,
|
||||||
|
page: page,
|
||||||
|
auth: context.defaultJwt(site.instanceHost)?.raw,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: l10n.communities_of_instance(siteView.site.name),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PullToRefresh(
|
||||||
|
onRefresh: () => context
|
||||||
|
.read<InstanceStore>()
|
||||||
|
.fetch(context.defaultJwt(site.instanceHost), refresh: true),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (siteView.site.description != null) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 15,
|
||||||
|
vertical: 15,
|
||||||
|
),
|
||||||
|
child: MarkdownText(
|
||||||
|
siteView.site.description!,
|
||||||
|
instanceHost: site.instanceHost,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Divider(),
|
||||||
|
],
|
||||||
|
SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
children: [
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
l10n.number_of_users_online(site.online),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
'${l10n.number_of_users(siteView.counts.usersActiveDay)} / ${l10n.day}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
'${l10n.number_of_users(siteView.counts.usersActiveWeek)} / ${l10n.week}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
'${l10n.number_of_users(siteView.counts.usersActiveMonth)} / ${l10n.month}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
'${l10n.number_of_users(siteView.counts.usersActiveHalfYear)} / ${l10n.six_months}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
l10n.number_of_users(siteView.counts.users),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
l10n.number_of_communities(siteView.counts.communities),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
l10n.number_of_posts(siteView.counts.posts),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: Text(
|
||||||
|
l10n.number_of_comments(siteView.counts.comments),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].spaced(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Divider(),
|
||||||
|
ListTile(
|
||||||
|
title: Center(
|
||||||
|
child: Text(
|
||||||
|
l10n.trending_communities,
|
||||||
|
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ObserverBuilder<InstanceStore>(
|
||||||
|
builder: (context, store) => store.communitiesState.map(
|
||||||
|
loading: () => const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
error: (errorTerm) => FailedToLoad(
|
||||||
|
refresh: () => store.fetchCommunites(
|
||||||
|
context.defaultJwt(store.instanceHost)),
|
||||||
|
message: errorTerm.tr(context),
|
||||||
|
),
|
||||||
|
data: (communities) => Column(
|
||||||
|
children: [
|
||||||
|
for (final c in communities)
|
||||||
|
ListTile(
|
||||||
|
onTap: () => Navigator.of(context).push(
|
||||||
|
CommunityPage.fromIdRoute(
|
||||||
|
store.instanceHost,
|
||||||
|
c.community.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(c.community.name),
|
||||||
|
leading: Avatar(url: c.community.icon),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Center(child: Text(l10n.see_all)),
|
||||||
|
onTap: goToCommunities,
|
||||||
|
),
|
||||||
|
const _Divider(),
|
||||||
|
ListTile(
|
||||||
|
title: Center(
|
||||||
|
child: Text(
|
||||||
|
l10n.admins,
|
||||||
|
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final u in site.admins)
|
||||||
|
PersonTile(
|
||||||
|
u.person,
|
||||||
|
expanded: true,
|
||||||
|
),
|
||||||
|
const _Divider(),
|
||||||
|
ListTile(
|
||||||
|
title: Center(child: Text(l10n.modlog)),
|
||||||
|
onTap: () => Navigator.of(context).push(
|
||||||
|
ModlogPage.forInstanceRoute(site.instanceHost),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Divider extends StatelessWidget {
|
||||||
|
const _Divider();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||||
|
child: Divider(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import '../../url_launcher.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../widgets/bottom_modal.dart';
|
||||||
|
import '../../widgets/info_table_popup.dart';
|
||||||
|
|
||||||
|
class InstanceMoreMenu extends StatelessWidget {
|
||||||
|
final FullSiteView site;
|
||||||
|
|
||||||
|
const InstanceMoreMenu({super.key, required this.site});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final instanceUrl = 'https://${site.instanceHost}';
|
||||||
|
final accountsStore = context.watch<AccountsStore>();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (!accountsStore.instances.contains(site.instanceHost))
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.add),
|
||||||
|
title: Text(L10n.of(context).add_instance),
|
||||||
|
onTap: () {
|
||||||
|
accountsStore.addInstance(site.instanceHost, assumeValid: true);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(
|
||||||
|
SnackBar(content: Text(L10n.of(context).instance_added)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.open_in_browser),
|
||||||
|
title: Text(L10n.of(context).open_in_browser),
|
||||||
|
onTap: () => launchLink(link: instanceUrl, context: context),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.info_outline),
|
||||||
|
title: Text(L10n.of(context).nerd_stuff),
|
||||||
|
onTap: () {
|
||||||
|
showInfoTablePopup(context: context, table: site.toJson());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void open(BuildContext context, FullSiteView site) {
|
||||||
|
showBottomModal(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => InstanceMoreMenu(site: site),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../util/async_store.dart';
|
||||||
|
|
||||||
|
part 'instance_store.g.dart';
|
||||||
|
|
||||||
|
class InstanceStore = _InstanceStore with _$InstanceStore;
|
||||||
|
|
||||||
|
abstract class _InstanceStore with Store {
|
||||||
|
final String instanceHost;
|
||||||
|
|
||||||
|
_InstanceStore(this.instanceHost);
|
||||||
|
|
||||||
|
final siteState = AsyncStore<FullSiteView>();
|
||||||
|
final communitiesState = AsyncStore<List<CommunityView>>();
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> fetch(Jwt? token, {bool refresh = false}) async {
|
||||||
|
await Future.wait([
|
||||||
|
siteState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
GetSite(auth: token?.raw),
|
||||||
|
refresh: refresh,
|
||||||
|
),
|
||||||
|
fetchCommunites(token, refresh: refresh),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> fetchCommunites(Jwt? token, {bool refresh = false}) async {
|
||||||
|
await communitiesState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
ListCommunities(
|
||||||
|
type: PostListingType.local,
|
||||||
|
sort: SortType.hot,
|
||||||
|
limit: 6,
|
||||||
|
auth: token?.raw,
|
||||||
|
),
|
||||||
|
refresh: refresh,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'instance_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
mixin _$InstanceStore on _InstanceStore, Store {
|
||||||
|
late final _$fetchAsyncAction =
|
||||||
|
AsyncAction('_InstanceStore.fetch', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> fetch(Jwt? token, {bool refresh = false}) {
|
||||||
|
return _$fetchAsyncAction.run(() => super.fetch(token, refresh: refresh));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$fetchCommunitesAsyncAction =
|
||||||
|
AsyncAction('_InstanceStore.fetchCommunites', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> fetchCommunites(Jwt? token, {bool refresh = false}) {
|
||||||
|
return _$fetchCommunitesAsyncAction
|
||||||
|
.run(() => super.fetchCommunites(token, refresh: refresh));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,11 +6,11 @@ part of 'log_console_page_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$LogConsolePageStore on _LogConsolePageStore, Store {
|
mixin _$LogConsolePageStore on _LogConsolePageStore, Store {
|
||||||
final _$_LogConsolePageStoreActionController =
|
late final _$_LogConsolePageStoreActionController =
|
||||||
ActionController(name: '_LogConsolePageStore');
|
ActionController(name: '_LogConsolePageStore', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addLog(LogRecord logRecord) {
|
void addLog(LogRecord logRecord) {
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:lemmy_api_client/pictrs.dart';
|
import 'package:lemmy_api_client/pictrs.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/image_picker.dart';
|
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../url_launcher.dart';
|
||||||
|
import '../util/files.dart';
|
||||||
import '../util/icons.dart';
|
import '../util/icons.dart';
|
||||||
import '../util/pictrs.dart';
|
import '../util/pictrs.dart';
|
||||||
import '../widgets/bottom_modal.dart';
|
import '../widgets/bottom_modal.dart';
|
||||||
import '../widgets/bottom_safe.dart';
|
import '../widgets/bottom_safe.dart';
|
||||||
import '../widgets/cached_network_image.dart';
|
import '../widgets/cached_network_image.dart';
|
||||||
import '../widgets/editor.dart';
|
import '../widgets/editor/editor.dart';
|
||||||
|
|
||||||
/// Page for managing things like username, email, avatar etc
|
/// Page for managing things like username, email, avatar etc
|
||||||
/// This page will assume the manage account is logged in and
|
/// This page will assume the manage account is logged in and
|
||||||
|
@ -48,13 +47,12 @@ class ManageAccountPage extends HookWidget {
|
||||||
final userProfileUrl =
|
final userProfileUrl =
|
||||||
await userFuture.then((e) => e.person.actorId);
|
await userFuture.then((e) => e.person.actorId);
|
||||||
|
|
||||||
if (await ul.canLaunch(userProfileUrl)) {
|
final didLaunch = await launchLink(
|
||||||
await ul.launch(userProfileUrl);
|
link: userProfileUrl,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
if (didLaunch) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text("can't open in browser")),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -88,7 +86,7 @@ class ManageAccountPage extends HookWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ManageAccount extends HookWidget {
|
class _ManageAccount extends HookWidget {
|
||||||
const _ManageAccount({Key? key, required this.user}) : super(key: key);
|
const _ManageAccount({required this.user});
|
||||||
|
|
||||||
final LocalUserSettingsView user;
|
final LocalUserSettingsView user;
|
||||||
|
|
||||||
|
@ -101,7 +99,6 @@ class _ManageAccount extends HookWidget {
|
||||||
|
|
||||||
final displayNameController =
|
final displayNameController =
|
||||||
useTextEditingController(text: user.person.displayName);
|
useTextEditingController(text: user.person.displayName);
|
||||||
final bioController = useTextEditingController(text: user.person.bio);
|
|
||||||
final emailController =
|
final emailController =
|
||||||
useTextEditingController(text: user.localUser.email);
|
useTextEditingController(text: user.localUser.email);
|
||||||
final matrixUserController =
|
final matrixUserController =
|
||||||
|
@ -124,13 +121,15 @@ class _ManageAccount extends HookWidget {
|
||||||
|
|
||||||
final deleteAccountPasswordController = useTextEditingController();
|
final deleteAccountPasswordController = useTextEditingController();
|
||||||
|
|
||||||
final bioFocusNode = useFocusNode();
|
|
||||||
final emailFocusNode = useFocusNode();
|
final emailFocusNode = useFocusNode();
|
||||||
final matrixUserFocusNode = useFocusNode();
|
final matrixUserFocusNode = useFocusNode();
|
||||||
final newPasswordFocusNode = useFocusNode();
|
final newPasswordFocusNode = useFocusNode();
|
||||||
// final verifyPasswordFocusNode = useFocusNode();
|
// final verifyPasswordFocusNode = useFocusNode();
|
||||||
// final oldPasswordFocusNode = useFocusNode();
|
// final oldPasswordFocusNode = useFocusNode();
|
||||||
|
|
||||||
|
final bioController = useEditorController(
|
||||||
|
instanceHost: user.instanceHost, text: user.person.bio);
|
||||||
|
|
||||||
final token =
|
final token =
|
||||||
accountsStore.userDataFor(user.instanceHost, user.person.name)!.jwt;
|
accountsStore.userDataFor(user.instanceHost, user.person.name)!.jwt;
|
||||||
|
|
||||||
|
@ -158,7 +157,9 @@ class _ManageAccount extends HookWidget {
|
||||||
displayName: displayNameController.text.isEmpty
|
displayName: displayNameController.text.isEmpty
|
||||||
? null
|
? null
|
||||||
: displayNameController.text,
|
: displayNameController.text,
|
||||||
bio: bioController.text.isEmpty ? null : bioController.text,
|
bio: bioController.textEditingController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: bioController.textEditingController.text,
|
||||||
email: emailController.text.isEmpty ? null : emailController.text,
|
email: emailController.text.isEmpty ? null : emailController.text,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -236,150 +237,157 @@ class _ManageAccount extends HookWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView(
|
return Stack(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
||||||
children: [
|
children: [
|
||||||
_ImagePicker(
|
ListView(
|
||||||
user: user,
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
name: L10n.of(context).avatar,
|
children: [
|
||||||
initialUrl: avatar.value,
|
_ImagePicker(
|
||||||
onChange: (value) => avatar.value = value,
|
user: user,
|
||||||
informAcceptedRef: informAcceptedAvatarRef,
|
name: L10n.of(context).avatar,
|
||||||
|
initialUrl: avatar.value,
|
||||||
|
onChange: (value) => avatar.value = value,
|
||||||
|
informAcceptedRef: informAcceptedAvatarRef,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_ImagePicker(
|
||||||
|
user: user,
|
||||||
|
name: L10n.of(context).banner,
|
||||||
|
initialUrl: banner.value,
|
||||||
|
onChange: (value) => banner.value = value,
|
||||||
|
informAcceptedRef: informAcceptedBannerRef,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(L10n.of(context).display_name,
|
||||||
|
style: theme.textTheme.headline6),
|
||||||
|
TextField(
|
||||||
|
controller: displayNameController,
|
||||||
|
onSubmitted: (_) => bioController.focusNode.requestFocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(L10n.of(context).bio, style: theme.textTheme.headline6),
|
||||||
|
Editor(
|
||||||
|
controller: bioController,
|
||||||
|
onSubmitted: (_) => emailFocusNode.requestFocus(),
|
||||||
|
maxLines: 10,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(L10n.of(context).email, style: theme.textTheme.headline6),
|
||||||
|
TextField(
|
||||||
|
focusNode: emailFocusNode,
|
||||||
|
controller: emailController,
|
||||||
|
autofillHints: const [AutofillHints.email],
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
onSubmitted: (_) => matrixUserFocusNode.requestFocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(L10n.of(context).matrix_user,
|
||||||
|
style: theme.textTheme.headline6),
|
||||||
|
TextField(
|
||||||
|
focusNode: matrixUserFocusNode,
|
||||||
|
controller: matrixUserController,
|
||||||
|
onSubmitted: (_) => newPasswordFocusNode.requestFocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Text(L10n.of(context)!.new_password, style: theme.textTheme.headline6),
|
||||||
|
// TextField(
|
||||||
|
// focusNode: newPasswordFocusNode,
|
||||||
|
// controller: newPasswordController,
|
||||||
|
// autofillHints: const [AutofillHints.newPassword],
|
||||||
|
// keyboardType: TextInputType.visiblePassword,
|
||||||
|
// obscureText: true,
|
||||||
|
// onSubmitted: (_) => verifyPasswordFocusNode.requestFocus(),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 8),
|
||||||
|
// Text(L10n.of(context)!.verify_password,
|
||||||
|
// style: theme.textTheme.headline6),
|
||||||
|
// TextField(
|
||||||
|
// focusNode: verifyPasswordFocusNode,
|
||||||
|
// controller: newPasswordVerifyController,
|
||||||
|
// autofillHints: const [AutofillHints.newPassword],
|
||||||
|
// keyboardType: TextInputType.visiblePassword,
|
||||||
|
// obscureText: true,
|
||||||
|
// onSubmitted: (_) => oldPasswordFocusNode.requestFocus(),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 8),
|
||||||
|
// Text(L10n.of(context)!.old_password, style: theme.textTheme.headline6),
|
||||||
|
// TextField(
|
||||||
|
// focusNode: oldPasswordFocusNode,
|
||||||
|
// controller: oldPasswordController,
|
||||||
|
// autofillHints: const [AutofillHints.password],
|
||||||
|
// keyboardType: TextInputType.visiblePassword,
|
||||||
|
// obscureText: true,
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 8),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: showNsfw.value,
|
||||||
|
onChanged: (checked) {
|
||||||
|
showNsfw.value = checked;
|
||||||
|
},
|
||||||
|
title: Text(L10n.of(context).show_nsfw),
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: botAccount.value,
|
||||||
|
onChanged: (checked) {
|
||||||
|
botAccount.value = checked;
|
||||||
|
},
|
||||||
|
title: Text(L10n.of(context).bot_account),
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: showBotAccounts.value,
|
||||||
|
onChanged: (checked) {
|
||||||
|
showBotAccounts.value = checked;
|
||||||
|
},
|
||||||
|
title: Text(L10n.of(context).show_bot_accounts),
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: showReadPosts.value,
|
||||||
|
onChanged: (checked) {
|
||||||
|
showReadPosts.value = checked;
|
||||||
|
},
|
||||||
|
title: Text(L10n.of(context).show_read_posts),
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: sendNotificationsToEmail.value,
|
||||||
|
onChanged: (checked) {
|
||||||
|
sendNotificationsToEmail.value = checked;
|
||||||
|
},
|
||||||
|
title: Text(L10n.of(context).send_notifications_to_email),
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: saveDelayedLoading.loading ? null : handleSubmit,
|
||||||
|
child: saveDelayedLoading.loading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
)
|
||||||
|
: Text(L10n.of(context).save),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: deleteAccountDialog,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
primary: Colors.red,
|
||||||
|
),
|
||||||
|
child: Text(L10n.of(context).delete_account.toUpperCase()),
|
||||||
|
),
|
||||||
|
const BottomSafe(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
BottomSticky(
|
||||||
_ImagePicker(
|
child: EditorToolbar(bioController),
|
||||||
user: user,
|
)
|
||||||
name: L10n.of(context).banner,
|
|
||||||
initialUrl: banner.value,
|
|
||||||
onChange: (value) => banner.value = value,
|
|
||||||
informAcceptedRef: informAcceptedBannerRef,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(L10n.of(context).display_name, style: theme.textTheme.headline6),
|
|
||||||
TextField(
|
|
||||||
controller: displayNameController,
|
|
||||||
onSubmitted: (_) => bioFocusNode.requestFocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(L10n.of(context).bio, style: theme.textTheme.headline6),
|
|
||||||
Editor(
|
|
||||||
controller: bioController,
|
|
||||||
focusNode: bioFocusNode,
|
|
||||||
onSubmitted: (_) => emailFocusNode.requestFocus(),
|
|
||||||
instanceHost: user.instanceHost,
|
|
||||||
maxLines: 10,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(L10n.of(context).email, style: theme.textTheme.headline6),
|
|
||||||
TextField(
|
|
||||||
focusNode: emailFocusNode,
|
|
||||||
controller: emailController,
|
|
||||||
autofillHints: const [AutofillHints.email],
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
onSubmitted: (_) => matrixUserFocusNode.requestFocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(L10n.of(context).matrix_user, style: theme.textTheme.headline6),
|
|
||||||
TextField(
|
|
||||||
focusNode: matrixUserFocusNode,
|
|
||||||
controller: matrixUserController,
|
|
||||||
onSubmitted: (_) => newPasswordFocusNode.requestFocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
// Text(L10n.of(context)!.new_password, style: theme.textTheme.headline6),
|
|
||||||
// TextField(
|
|
||||||
// focusNode: newPasswordFocusNode,
|
|
||||||
// controller: newPasswordController,
|
|
||||||
// autofillHints: const [AutofillHints.newPassword],
|
|
||||||
// keyboardType: TextInputType.visiblePassword,
|
|
||||||
// obscureText: true,
|
|
||||||
// onSubmitted: (_) => verifyPasswordFocusNode.requestFocus(),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 8),
|
|
||||||
// Text(L10n.of(context)!.verify_password,
|
|
||||||
// style: theme.textTheme.headline6),
|
|
||||||
// TextField(
|
|
||||||
// focusNode: verifyPasswordFocusNode,
|
|
||||||
// controller: newPasswordVerifyController,
|
|
||||||
// autofillHints: const [AutofillHints.newPassword],
|
|
||||||
// keyboardType: TextInputType.visiblePassword,
|
|
||||||
// obscureText: true,
|
|
||||||
// onSubmitted: (_) => oldPasswordFocusNode.requestFocus(),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 8),
|
|
||||||
// Text(L10n.of(context)!.old_password, style: theme.textTheme.headline6),
|
|
||||||
// TextField(
|
|
||||||
// focusNode: oldPasswordFocusNode,
|
|
||||||
// controller: oldPasswordController,
|
|
||||||
// autofillHints: const [AutofillHints.password],
|
|
||||||
// keyboardType: TextInputType.visiblePassword,
|
|
||||||
// obscureText: true,
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 8),
|
|
||||||
SwitchListTile.adaptive(
|
|
||||||
value: showNsfw.value,
|
|
||||||
onChanged: (checked) {
|
|
||||||
showNsfw.value = checked;
|
|
||||||
},
|
|
||||||
title: Text(L10n.of(context).show_nsfw),
|
|
||||||
dense: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SwitchListTile.adaptive(
|
|
||||||
value: botAccount.value,
|
|
||||||
onChanged: (checked) {
|
|
||||||
botAccount.value = checked;
|
|
||||||
},
|
|
||||||
title: Text(L10n.of(context).bot_account),
|
|
||||||
dense: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SwitchListTile.adaptive(
|
|
||||||
value: showBotAccounts.value,
|
|
||||||
onChanged: (checked) {
|
|
||||||
showBotAccounts.value = checked;
|
|
||||||
},
|
|
||||||
title: Text(L10n.of(context).show_bot_accounts),
|
|
||||||
dense: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SwitchListTile.adaptive(
|
|
||||||
value: showReadPosts.value,
|
|
||||||
onChanged: (checked) {
|
|
||||||
showReadPosts.value = checked;
|
|
||||||
},
|
|
||||||
title: Text(L10n.of(context).show_read_posts),
|
|
||||||
dense: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SwitchListTile.adaptive(
|
|
||||||
value: sendNotificationsToEmail.value,
|
|
||||||
onChanged: (checked) {
|
|
||||||
sendNotificationsToEmail.value = checked;
|
|
||||||
},
|
|
||||||
title: Text(L10n.of(context).send_notifications_to_email),
|
|
||||||
dense: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: saveDelayedLoading.loading ? null : handleSubmit,
|
|
||||||
child: saveDelayedLoading.loading
|
|
||||||
? const SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator.adaptive(),
|
|
||||||
)
|
|
||||||
: Text(L10n.of(context).save),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: deleteAccountDialog,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
primary: Colors.red,
|
|
||||||
),
|
|
||||||
child: Text(L10n.of(context).delete_account.toUpperCase()),
|
|
||||||
),
|
|
||||||
const BottomSafe(),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -398,13 +406,12 @@ class _ImagePicker extends HookWidget {
|
||||||
final ObjectRef<VoidCallback?> informAcceptedRef;
|
final ObjectRef<VoidCallback?> informAcceptedRef;
|
||||||
|
|
||||||
const _ImagePicker({
|
const _ImagePicker({
|
||||||
Key? key,
|
|
||||||
required this.initialUrl,
|
required this.initialUrl,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.user,
|
required this.user,
|
||||||
required this.onChange,
|
required this.onChange,
|
||||||
required this.informAcceptedRef,
|
required this.informAcceptedRef,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -415,13 +422,12 @@ class _ImagePicker extends HookWidget {
|
||||||
final url = useState(initialUrl.value);
|
final url = useState(initialUrl.value);
|
||||||
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
|
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
|
||||||
|
|
||||||
final imagePicker = useImagePicker();
|
|
||||||
final accountsStore = useAccountsStore();
|
final accountsStore = useAccountsStore();
|
||||||
final delayedLoading = useDelayedLoading();
|
final delayedLoading = useDelayedLoading();
|
||||||
|
|
||||||
uploadImage() async {
|
uploadImage() async {
|
||||||
try {
|
try {
|
||||||
final pic = await imagePicker.pickImage(source: ImageSource.gallery);
|
final pic = await pickImage();
|
||||||
// pic is null when the picker was cancelled
|
// pic is null when the picker was cancelled
|
||||||
if (pic != null) {
|
if (pic != null) {
|
||||||
delayedLoading.start();
|
delayedLoading.start();
|
||||||
|
|
|
@ -12,6 +12,7 @@ abstract class _ModlogPageStore with Store, DisposableStore {
|
||||||
final String instanceHost;
|
final String instanceHost;
|
||||||
final int? communityId;
|
final int? communityId;
|
||||||
|
|
||||||
|
// ignore: unused_element
|
||||||
_ModlogPageStore(this.instanceHost, [this.communityId]) {
|
_ModlogPageStore(this.instanceHost, [this.communityId]) {
|
||||||
addReaction(reaction((_) => page, (_) => fetchPage()));
|
addReaction(reaction((_) => page, (_) => fetchPage()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ part of 'modlog_page_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$ModlogPageStore on _ModlogPageStore, Store {
|
mixin _$ModlogPageStore on _ModlogPageStore, Store {
|
||||||
Computed<bool>? _$hasPreviousPageComputed;
|
Computed<bool>? _$hasPreviousPageComputed;
|
||||||
|
@ -24,7 +24,7 @@ mixin _$ModlogPageStore on _ModlogPageStore, Store {
|
||||||
name: '_ModlogPageStore.hasNextPage'))
|
name: '_ModlogPageStore.hasNextPage'))
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
final _$pageAtom = Atom(name: '_ModlogPageStore.page');
|
late final _$pageAtom = Atom(name: '_ModlogPageStore.page', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get page {
|
int get page {
|
||||||
|
@ -39,15 +39,16 @@ mixin _$ModlogPageStore on _ModlogPageStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$fetchPageAsyncAction = AsyncAction('_ModlogPageStore.fetchPage');
|
late final _$fetchPageAsyncAction =
|
||||||
|
AsyncAction('_ModlogPageStore.fetchPage', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> fetchPage() {
|
Future<void> fetchPage() {
|
||||||
return _$fetchPageAsyncAction.run(() => super.fetchPage());
|
return _$fetchPageAsyncAction.run(() => super.fetchPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_ModlogPageStoreActionController =
|
late final _$_ModlogPageStoreActionController =
|
||||||
ActionController(name: '_ModlogPageStore');
|
ActionController(name: '_ModlogPageStore', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void previousPage() {
|
void previousPage() {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import '../../widgets/avatar.dart';
|
||||||
import 'modlog_entry.dart';
|
import 'modlog_entry.dart';
|
||||||
|
|
||||||
class ModlogTable extends StatelessWidget {
|
class ModlogTable extends StatelessWidget {
|
||||||
const ModlogTable({Key? key, required this.modlog}) : super(key: key);
|
const ModlogTable({super.key, required this.modlog});
|
||||||
|
|
||||||
final Modlog modlog;
|
final Modlog modlog;
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
|
||||||
|
|
||||||
import '../../hooks/delayed_loading.dart';
|
import '../../hooks/delayed_loading.dart';
|
||||||
import '../../hooks/stores.dart';
|
import '../../hooks/stores.dart';
|
||||||
import '../../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
import '../../stores/accounts_store.dart';
|
import '../../stores/accounts_store.dart';
|
||||||
import '../../stores/config_store.dart';
|
import '../../stores/config_store.dart';
|
||||||
|
import '../../url_launcher.dart';
|
||||||
import '../../widgets/cached_network_image.dart';
|
import '../../widgets/cached_network_image.dart';
|
||||||
import '../../widgets/fullscreenable_image.dart';
|
import '../../widgets/fullscreenable_image.dart';
|
||||||
import '../../widgets/radio_picker.dart';
|
import '../../widgets/radio_picker.dart';
|
||||||
|
@ -173,8 +173,11 @@ class AddAccountPage extends HookWidget {
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// TODO: extract to LemmyUrls or something
|
launchLink(
|
||||||
ul.launch('https://${selectedInstance.value}/login');
|
// TODO: extract to LemmyUrls or something
|
||||||
|
link: 'https://${selectedInstance.value}/login',
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Register'),
|
child: const Text('Register'),
|
||||||
),
|
),
|
||||||
|
|
|
@ -10,7 +10,7 @@ import 'community_block_store.dart';
|
||||||
import 'user_block_store.dart';
|
import 'user_block_store.dart';
|
||||||
|
|
||||||
class BlockPersonTile extends StatelessWidget {
|
class BlockPersonTile extends StatelessWidget {
|
||||||
const BlockPersonTile({Key? key}) : super(key: key);
|
const BlockPersonTile({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -40,7 +40,7 @@ class BlockPersonTile extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class BlockCommunityTile extends HookWidget {
|
class BlockCommunityTile extends HookWidget {
|
||||||
const BlockCommunityTile({Key? key}) : super(key: key);
|
const BlockCommunityTile({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ part of 'blocks_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$BlocksStore on _BlocksStore, Store {
|
mixin _$BlocksStore on _BlocksStore, Store {
|
||||||
Computed<Iterable<UserBlockStore>?>? _$blockedUsersComputed;
|
Computed<Iterable<UserBlockStore>?>? _$blockedUsersComputed;
|
||||||
|
@ -32,7 +32,8 @@ mixin _$BlocksStore on _BlocksStore, Store {
|
||||||
Computed<bool>(() => super.isUsable, name: '_BlocksStore.isUsable'))
|
Computed<bool>(() => super.isUsable, name: '_BlocksStore.isUsable'))
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
final _$_blockedUsersAtom = Atom(name: '_BlocksStore._blockedUsers');
|
late final _$_blockedUsersAtom =
|
||||||
|
Atom(name: '_BlocksStore._blockedUsers', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ObservableList<UserBlockStore>? get _blockedUsers {
|
ObservableList<UserBlockStore>? get _blockedUsers {
|
||||||
|
@ -47,8 +48,8 @@ mixin _$BlocksStore on _BlocksStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_blockedCommunitiesAtom =
|
late final _$_blockedCommunitiesAtom =
|
||||||
Atom(name: '_BlocksStore._blockedCommunities');
|
Atom(name: '_BlocksStore._blockedCommunities', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ObservableList<CommunityBlockStore>? get _blockedCommunities {
|
ObservableList<CommunityBlockStore>? get _blockedCommunities {
|
||||||
|
@ -63,15 +64,16 @@ mixin _$BlocksStore on _BlocksStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$blockUserAsyncAction = AsyncAction('_BlocksStore.blockUser');
|
late final _$blockUserAsyncAction =
|
||||||
|
AsyncAction('_BlocksStore.blockUser', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> blockUser(Jwt token, int id) {
|
Future<void> blockUser(Jwt token, int id) {
|
||||||
return _$blockUserAsyncAction.run(() => super.blockUser(token, id));
|
return _$blockUserAsyncAction.run(() => super.blockUser(token, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$blockCommunityAsyncAction =
|
late final _$blockCommunityAsyncAction =
|
||||||
AsyncAction('_BlocksStore.blockCommunity');
|
AsyncAction('_BlocksStore.blockCommunity', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> blockCommunity(Jwt token, int id) {
|
Future<void> blockCommunity(Jwt token, int id) {
|
||||||
|
@ -79,7 +81,8 @@ mixin _$BlocksStore on _BlocksStore, Store {
|
||||||
.run(() => super.blockCommunity(token, id));
|
.run(() => super.blockCommunity(token, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$refreshAsyncAction = AsyncAction('_BlocksStore.refresh');
|
late final _$refreshAsyncAction =
|
||||||
|
AsyncAction('_BlocksStore.refresh', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> refresh() {
|
Future<void> refresh() {
|
||||||
|
|
|
@ -6,10 +6,11 @@ part of 'community_block_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$CommunityBlockStore on _CommunityBlockStore, Store {
|
mixin _$CommunityBlockStore on _CommunityBlockStore, Store {
|
||||||
final _$blockedAtom = Atom(name: '_CommunityBlockStore.blocked');
|
late final _$blockedAtom =
|
||||||
|
Atom(name: '_CommunityBlockStore.blocked', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get blocked {
|
bool get blocked {
|
||||||
|
|
|
@ -6,10 +6,11 @@ part of 'user_block_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$UserBlockStore on _UserBlockStore, Store {
|
mixin _$UserBlockStore on _UserBlockStore, Store {
|
||||||
final _$blockedAtom = Atom(name: '_UserBlockStore.blocked');
|
late final _$blockedAtom =
|
||||||
|
Atom(name: '_UserBlockStore.blocked', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get blocked {
|
bool get blocked {
|
||||||
|
|
|
@ -185,10 +185,9 @@ class _AccountOptions extends HookWidget {
|
||||||
final String username;
|
final String username;
|
||||||
|
|
||||||
const _AccountOptions({
|
const _AccountOptions({
|
||||||
Key? key,
|
|
||||||
required this.instanceHost,
|
required this.instanceHost,
|
||||||
required this.username,
|
required this.username,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -4,29 +4,34 @@ import 'package:lemmy_api_client/v3.dart';
|
||||||
import '../util/extensions/api.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
import '../widgets/avatar.dart';
|
import '../widgets/avatar.dart';
|
||||||
|
import '../widgets/infinite_scroll.dart';
|
||||||
import '../widgets/markdown_text.dart';
|
import '../widgets/markdown_text.dart';
|
||||||
|
|
||||||
/// Infinite list of Users fetched by the given fetcher
|
/// Infinite list of Users fetched by the given fetcher
|
||||||
class UsersListPage extends StatelessWidget {
|
class UsersListPage extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final List<PersonViewSafe> users;
|
final Fetcher<PersonViewSafe> fetcher;
|
||||||
|
|
||||||
const UsersListPage({Key? key, required this.users, this.title = ''})
|
const UsersListPage({super.key, required this.fetcher, this.title = ''});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
// TODO: change to infinite scroll
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: theme.cardColor,
|
backgroundColor: theme.cardColor,
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: InfiniteScroll<PersonViewSafe>(
|
||||||
itemBuilder: (context, i) => UsersListItem(user: users[i]),
|
fetcher: fetcher,
|
||||||
itemCount: users.length,
|
itemBuilder: (user) => Column(
|
||||||
|
children: [
|
||||||
|
const Divider(),
|
||||||
|
UsersListItem(user: user),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
uniqueProp: (user) => user.person.actorId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,21 +40,23 @@ class UsersListPage extends StatelessWidget {
|
||||||
class UsersListItem extends StatelessWidget {
|
class UsersListItem extends StatelessWidget {
|
||||||
final PersonViewSafe user;
|
final PersonViewSafe user;
|
||||||
|
|
||||||
const UsersListItem({Key? key, required this.user}) : super(key: key);
|
const UsersListItem({super.key, required this.user});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ListTile(
|
Widget build(BuildContext context) {
|
||||||
title: Text(user.person.originPreferredName),
|
return ListTile(
|
||||||
subtitle: user.person.bio != null
|
title: Text(user.person.originPreferredName),
|
||||||
? Opacity(
|
subtitle: user.person.bio != null
|
||||||
opacity: 0.7,
|
? Opacity(
|
||||||
child: MarkdownText(
|
opacity: 0.7,
|
||||||
user.person.bio!,
|
child: MarkdownText(
|
||||||
instanceHost: user.instanceHost,
|
user.person.bio!,
|
||||||
),
|
instanceHost: user.instanceHost,
|
||||||
)
|
),
|
||||||
: null,
|
)
|
||||||
onTap: () => goToUser.fromPersonSafe(context, user.person),
|
: null,
|
||||||
leading: Avatar(url: user.person.avatar),
|
onTap: () => goToUser.fromPersonSafe(context, user.person),
|
||||||
);
|
leading: Avatar(url: user.person.avatar),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,5 @@
|
||||||
const lemmurRepositoryLink = 'https://github.com/LemmurOrg/lemmur';
|
const lemmurRepositoryUrl = 'https://github.com/LemmurOrg/lemmur';
|
||||||
|
const patreonUrl = 'https://patreon.com/lemmur';
|
||||||
|
const buyMeACoffeeUrl = 'https://buymeacoff.ee/lemmur';
|
||||||
|
const markdownGuide =
|
||||||
|
'https://join-lemmy.org/docs/en/about/guide.html#using-markdown';
|
||||||
|
|
|
@ -19,7 +19,7 @@ ConfigStore _$ConfigStoreFromJson(Map<String, dynamic> json) => ConfigStore()
|
||||||
|
|
||||||
Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
|
Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'theme': _$ThemeModeEnumMap[instance.theme],
|
'theme': _$ThemeModeEnumMap[instance.theme]!,
|
||||||
'amoledDarkMode': instance.amoledDarkMode,
|
'amoledDarkMode': instance.amoledDarkMode,
|
||||||
'locale': const LocaleConverter().toJson(instance.locale),
|
'locale': const LocaleConverter().toJson(instance.locale),
|
||||||
'showAvatars': instance.showAvatars,
|
'showAvatars': instance.showAvatars,
|
||||||
|
@ -38,10 +38,10 @@ const _$ThemeModeEnumMap = {
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$ConfigStore on _ConfigStore, Store {
|
mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
final _$themeAtom = Atom(name: '_ConfigStore.theme');
|
late final _$themeAtom = Atom(name: '_ConfigStore.theme', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ThemeMode get theme {
|
ThemeMode get theme {
|
||||||
|
@ -56,7 +56,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$amoledDarkModeAtom = Atom(name: '_ConfigStore.amoledDarkMode');
|
late final _$amoledDarkModeAtom =
|
||||||
|
Atom(name: '_ConfigStore.amoledDarkMode', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get amoledDarkMode {
|
bool get amoledDarkMode {
|
||||||
|
@ -71,7 +72,7 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$localeAtom = Atom(name: '_ConfigStore.locale');
|
late final _$localeAtom = Atom(name: '_ConfigStore.locale', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Locale get locale {
|
Locale get locale {
|
||||||
|
@ -86,7 +87,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$showAvatarsAtom = Atom(name: '_ConfigStore.showAvatars');
|
late final _$showAvatarsAtom =
|
||||||
|
Atom(name: '_ConfigStore.showAvatars', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get showAvatars {
|
bool get showAvatars {
|
||||||
|
@ -101,7 +103,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$showScoresAtom = Atom(name: '_ConfigStore.showScores');
|
late final _$showScoresAtom =
|
||||||
|
Atom(name: '_ConfigStore.showScores', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get showScores {
|
bool get showScores {
|
||||||
|
@ -116,7 +119,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$defaultSortTypeAtom = Atom(name: '_ConfigStore.defaultSortType');
|
late final _$defaultSortTypeAtom =
|
||||||
|
Atom(name: '_ConfigStore.defaultSortType', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SortType get defaultSortType {
|
SortType get defaultSortType {
|
||||||
|
@ -131,8 +135,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$defaultListingTypeAtom =
|
late final _$defaultListingTypeAtom =
|
||||||
Atom(name: '_ConfigStore.defaultListingType');
|
Atom(name: '_ConfigStore.defaultListingType', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PostListingType get defaultListingType {
|
PostListingType get defaultListingType {
|
||||||
|
@ -147,8 +151,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$importLemmyUserSettingsAsyncAction =
|
late final _$importLemmyUserSettingsAsyncAction =
|
||||||
AsyncAction('_ConfigStore.importLemmyUserSettings');
|
AsyncAction('_ConfigStore.importLemmyUserSettings', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> importLemmyUserSettings(Jwt token) {
|
Future<void> importLemmyUserSettings(Jwt token) {
|
||||||
|
@ -156,7 +160,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
||||||
.run(() => super.importLemmyUserSettings(token));
|
.run(() => super.importLemmyUserSettings(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_ConfigStoreActionController = ActionController(name: '_ConfigStore');
|
late final _$_ConfigStoreActionController =
|
||||||
|
ActionController(name: '_ConfigStore', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void copyLemmyUserSettings(LocalUserSettings localUserSettings) {
|
void copyLemmyUserSettings(LocalUserSettings localUserSettings) {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||||
|
|
||||||
|
import 'l10n/l10n.dart';
|
||||||
import 'pages/community/community.dart';
|
import 'pages/community/community.dart';
|
||||||
import 'pages/instance.dart';
|
import 'pages/instance/instance.dart';
|
||||||
import 'pages/media_view.dart';
|
import 'pages/media_view.dart';
|
||||||
import 'pages/user.dart';
|
import 'pages/user.dart';
|
||||||
import 'stores/accounts_store.dart';
|
import 'stores/accounts_store.dart';
|
||||||
|
@ -23,7 +25,10 @@ Future<void> linkLauncher({
|
||||||
final instances = context.read<AccountsStore>().instances;
|
final instances = context.read<AccountsStore>().instances;
|
||||||
|
|
||||||
final chonks = url.split('/');
|
final chonks = url.split('/');
|
||||||
if (chonks.length == 1) return openInBrowser(url);
|
if (chonks.length == 1) {
|
||||||
|
await launchLink(link: url, context: context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// CHECK IF LINK TO USER
|
// CHECK IF LINK TO USER
|
||||||
if (url.startsWith('/u/')) {
|
if (url.startsWith('/u/')) {
|
||||||
|
@ -48,7 +53,7 @@ Future<void> linkLauncher({
|
||||||
|
|
||||||
if (matchedInstance != null && instances.any((e) => e == match?.group(1))) {
|
if (matchedInstance != null && instances.any((e) => e == match?.group(1))) {
|
||||||
if (rest == null || rest.isEmpty || rest == '/') {
|
if (rest == null || rest.isEmpty || rest == '/') {
|
||||||
return push(() => InstancePage(instanceHost: matchedInstance));
|
return Navigator.of(context).push<void>(InstancePage.route(instanceHost));
|
||||||
}
|
}
|
||||||
final split = rest.split('/');
|
final split = rest.split('/');
|
||||||
switch (split[1]) {
|
switch (split[1]) {
|
||||||
|
@ -97,14 +102,25 @@ Future<void> linkLauncher({
|
||||||
|
|
||||||
// FALLBACK TO REGULAR LINK STUFF
|
// FALLBACK TO REGULAR LINK STUFF
|
||||||
|
|
||||||
return openInBrowser(url);
|
await launchLink(link: url, context: context);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openInBrowser(String url) async {
|
final _logger = Logger('launchLink');
|
||||||
if (await ul.canLaunch(url)) {
|
|
||||||
await ul.launch(url);
|
/// Returns whether launching was successful.
|
||||||
|
Future<bool> launchLink({
|
||||||
|
required String link,
|
||||||
|
required BuildContext context,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.tryParse(link);
|
||||||
|
if (uri != null && await ul.canLaunchUrl(uri)) {
|
||||||
|
await ul.launchUrl(uri);
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw Exception();
|
_logger.warning('Failed to launch a link: $link');
|
||||||
// TODO: handle opening links to stuff in app
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(L10n.of(context).cannot_open_in_browser)),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../l10n/l10n_from_string.dart';
|
||||||
|
|
||||||
part 'async_store.freezed.dart';
|
part 'async_store.freezed.dart';
|
||||||
part 'async_store.g.dart';
|
part 'async_store.g.dart';
|
||||||
|
|
||||||
|
@ -28,6 +30,10 @@ abstract class _AsyncStore<T> with Store {
|
||||||
@action
|
@action
|
||||||
void setData(T data) => asyncState = AsyncState.data(data);
|
void setData(T data) => asyncState = AsyncState.data(data);
|
||||||
|
|
||||||
|
/// reset an asyncState to its initial one
|
||||||
|
@action
|
||||||
|
void reset() => asyncState = AsyncState<T>.initial();
|
||||||
|
|
||||||
/// runs some async action and reflects the progress in [asyncState].
|
/// runs some async action and reflects the progress in [asyncState].
|
||||||
/// If successful, the result is returned, otherwise null is returned.
|
/// If successful, the result is returned, otherwise null is returned.
|
||||||
/// If this [AsyncStore] is already running some action, it will exit immediately and do nothing
|
/// If this [AsyncStore] is already running some action, it will exit immediately and do nothing
|
||||||
|
@ -51,11 +57,10 @@ abstract class _AsyncStore<T> with Store {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} on SocketException {
|
} on SocketException {
|
||||||
// TODO: use an existing l10n key
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
asyncState = data.copyWith(errorTerm: 'network_error');
|
asyncState = data.copyWith(errorTerm: L10nStrings.network_error);
|
||||||
} else {
|
} else {
|
||||||
asyncState = const AsyncState.error('network_error');
|
asyncState = const AsyncState.error(L10nStrings.network_error);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
|
@ -65,6 +70,8 @@ abstract class _AsyncStore<T> with Store {
|
||||||
}
|
}
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [run] but specialized for a [LemmyApiQuery].
|
/// [run] but specialized for a [LemmyApiQuery].
|
||||||
|
@ -76,8 +83,10 @@ abstract class _AsyncStore<T> with Store {
|
||||||
bool refresh = false,
|
bool refresh = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
return await run(() => LemmyApiV3(instanceHost).run(query),
|
return await run(
|
||||||
refresh: refresh);
|
() => LemmyApiV3(instanceHost).run(query),
|
||||||
|
refresh: refresh,
|
||||||
|
);
|
||||||
} on LemmyApiException catch (err) {
|
} on LemmyApiException catch (err) {
|
||||||
final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null;
|
final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null;
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
|
@ -86,6 +95,8 @@ abstract class _AsyncStore<T> with Store {
|
||||||
asyncState = AsyncState<T>.error(err.message);
|
asyncState = AsyncState<T>.error(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// helper function for mapping [asyncState] into 3 variants
|
/// helper function for mapping [asyncState] into 3 variants
|
||||||
|
|
|
@ -12,36 +12,7 @@ part of 'async_store.dart';
|
||||||
T _$identity<T>(T value) => value;
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
final _privateConstructorUsedError = UnsupportedError(
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
class _$AsyncStateTearOff {
|
|
||||||
const _$AsyncStateTearOff();
|
|
||||||
|
|
||||||
AsyncStateInitial<T> initial<T>() {
|
|
||||||
return AsyncStateInitial<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncStateData<T> data<T>(T data, [String? errorTerm]) {
|
|
||||||
return AsyncStateData<T>(
|
|
||||||
data,
|
|
||||||
errorTerm,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncStateLoading<T> loading<T>() {
|
|
||||||
return AsyncStateLoading<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncStateError<T> error<T>(String errorTerm) {
|
|
||||||
return AsyncStateError<T>(
|
|
||||||
errorTerm,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
const $AsyncState = _$AsyncStateTearOff();
|
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$AsyncState<T> {
|
mixin _$AsyncState<T> {
|
||||||
|
@ -115,22 +86,22 @@ class _$AsyncStateCopyWithImpl<T, $Res>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract class $AsyncStateInitialCopyWith<T, $Res> {
|
abstract class _$$AsyncStateInitialCopyWith<T, $Res> {
|
||||||
factory $AsyncStateInitialCopyWith(AsyncStateInitial<T> value,
|
factory _$$AsyncStateInitialCopyWith(_$AsyncStateInitial<T> value,
|
||||||
$Res Function(AsyncStateInitial<T>) then) =
|
$Res Function(_$AsyncStateInitial<T>) then) =
|
||||||
_$AsyncStateInitialCopyWithImpl<T, $Res>;
|
__$$AsyncStateInitialCopyWithImpl<T, $Res>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class _$AsyncStateInitialCopyWithImpl<T, $Res>
|
class __$$AsyncStateInitialCopyWithImpl<T, $Res>
|
||||||
extends _$AsyncStateCopyWithImpl<T, $Res>
|
extends _$AsyncStateCopyWithImpl<T, $Res>
|
||||||
implements $AsyncStateInitialCopyWith<T, $Res> {
|
implements _$$AsyncStateInitialCopyWith<T, $Res> {
|
||||||
_$AsyncStateInitialCopyWithImpl(
|
__$$AsyncStateInitialCopyWithImpl(_$AsyncStateInitial<T> _value,
|
||||||
AsyncStateInitial<T> _value, $Res Function(AsyncStateInitial<T>) _then)
|
$Res Function(_$AsyncStateInitial<T>) _then)
|
||||||
: super(_value, (v) => _then(v as AsyncStateInitial<T>));
|
: super(_value, (v) => _then(v as _$AsyncStateInitial<T>));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AsyncStateInitial<T> get _value => super._value as AsyncStateInitial<T>;
|
_$AsyncStateInitial<T> get _value => super._value as _$AsyncStateInitial<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@ -148,13 +119,13 @@ class _$AsyncStateInitial<T>
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties..add(DiagnosticsProperty('type', 'AsyncState<$T>.initial'));
|
properties.add(DiagnosticsProperty('type', 'AsyncState<$T>.initial'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType && other is AsyncStateInitial<T>);
|
(other.runtimeType == runtimeType && other is _$AsyncStateInitial<T>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -240,30 +211,30 @@ abstract class AsyncStateInitial<T> implements AsyncState<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract class $AsyncStateDataCopyWith<T, $Res> {
|
abstract class _$$AsyncStateDataCopyWith<T, $Res> {
|
||||||
factory $AsyncStateDataCopyWith(
|
factory _$$AsyncStateDataCopyWith(
|
||||||
AsyncStateData<T> value, $Res Function(AsyncStateData<T>) then) =
|
_$AsyncStateData<T> value, $Res Function(_$AsyncStateData<T>) then) =
|
||||||
_$AsyncStateDataCopyWithImpl<T, $Res>;
|
__$$AsyncStateDataCopyWithImpl<T, $Res>;
|
||||||
$Res call({T data, String? errorTerm});
|
$Res call({T data, String? errorTerm});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class _$AsyncStateDataCopyWithImpl<T, $Res>
|
class __$$AsyncStateDataCopyWithImpl<T, $Res>
|
||||||
extends _$AsyncStateCopyWithImpl<T, $Res>
|
extends _$AsyncStateCopyWithImpl<T, $Res>
|
||||||
implements $AsyncStateDataCopyWith<T, $Res> {
|
implements _$$AsyncStateDataCopyWith<T, $Res> {
|
||||||
_$AsyncStateDataCopyWithImpl(
|
__$$AsyncStateDataCopyWithImpl(
|
||||||
AsyncStateData<T> _value, $Res Function(AsyncStateData<T>) _then)
|
_$AsyncStateData<T> _value, $Res Function(_$AsyncStateData<T>) _then)
|
||||||
: super(_value, (v) => _then(v as AsyncStateData<T>));
|
: super(_value, (v) => _then(v as _$AsyncStateData<T>));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AsyncStateData<T> get _value => super._value as AsyncStateData<T>;
|
_$AsyncStateData<T> get _value => super._value as _$AsyncStateData<T>;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? data = freezed,
|
Object? data = freezed,
|
||||||
Object? errorTerm = freezed,
|
Object? errorTerm = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(AsyncStateData<T>(
|
return _then(_$AsyncStateData<T>(
|
||||||
data == freezed
|
data == freezed
|
||||||
? _value.data
|
? _value.data
|
||||||
: data // ignore: cast_nullable_to_non_nullable
|
: data // ignore: cast_nullable_to_non_nullable
|
||||||
|
@ -306,7 +277,7 @@ class _$AsyncStateData<T>
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is AsyncStateData<T> &&
|
other is _$AsyncStateData<T> &&
|
||||||
const DeepCollectionEquality().equals(other.data, data) &&
|
const DeepCollectionEquality().equals(other.data, data) &&
|
||||||
const DeepCollectionEquality().equals(other.errorTerm, errorTerm));
|
const DeepCollectionEquality().equals(other.errorTerm, errorTerm));
|
||||||
}
|
}
|
||||||
|
@ -319,8 +290,8 @@ class _$AsyncStateData<T>
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
$AsyncStateDataCopyWith<T, AsyncStateData<T>> get copyWith =>
|
_$$AsyncStateDataCopyWith<T, _$AsyncStateData<T>> get copyWith =>
|
||||||
_$AsyncStateDataCopyWithImpl<T, AsyncStateData<T>>(this, _$identity);
|
__$$AsyncStateDataCopyWithImpl<T, _$AsyncStateData<T>>(this, _$identity);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
|
@ -398,33 +369,33 @@ class _$AsyncStateData<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class AsyncStateData<T> implements AsyncState<T> {
|
abstract class AsyncStateData<T> implements AsyncState<T> {
|
||||||
const factory AsyncStateData(T data, [String? errorTerm]) =
|
const factory AsyncStateData(final T data, [final String? errorTerm]) =
|
||||||
_$AsyncStateData<T>;
|
_$AsyncStateData<T>;
|
||||||
|
|
||||||
T get data;
|
T get data;
|
||||||
String? get errorTerm;
|
String? get errorTerm;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$AsyncStateDataCopyWith<T, AsyncStateData<T>> get copyWith =>
|
_$$AsyncStateDataCopyWith<T, _$AsyncStateData<T>> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract class $AsyncStateLoadingCopyWith<T, $Res> {
|
abstract class _$$AsyncStateLoadingCopyWith<T, $Res> {
|
||||||
factory $AsyncStateLoadingCopyWith(AsyncStateLoading<T> value,
|
factory _$$AsyncStateLoadingCopyWith(_$AsyncStateLoading<T> value,
|
||||||
$Res Function(AsyncStateLoading<T>) then) =
|
$Res Function(_$AsyncStateLoading<T>) then) =
|
||||||
_$AsyncStateLoadingCopyWithImpl<T, $Res>;
|
__$$AsyncStateLoadingCopyWithImpl<T, $Res>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class _$AsyncStateLoadingCopyWithImpl<T, $Res>
|
class __$$AsyncStateLoadingCopyWithImpl<T, $Res>
|
||||||
extends _$AsyncStateCopyWithImpl<T, $Res>
|
extends _$AsyncStateCopyWithImpl<T, $Res>
|
||||||
implements $AsyncStateLoadingCopyWith<T, $Res> {
|
implements _$$AsyncStateLoadingCopyWith<T, $Res> {
|
||||||
_$AsyncStateLoadingCopyWithImpl(
|
__$$AsyncStateLoadingCopyWithImpl(_$AsyncStateLoading<T> _value,
|
||||||
AsyncStateLoading<T> _value, $Res Function(AsyncStateLoading<T>) _then)
|
$Res Function(_$AsyncStateLoading<T>) _then)
|
||||||
: super(_value, (v) => _then(v as AsyncStateLoading<T>));
|
: super(_value, (v) => _then(v as _$AsyncStateLoading<T>));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AsyncStateLoading<T> get _value => super._value as AsyncStateLoading<T>;
|
_$AsyncStateLoading<T> get _value => super._value as _$AsyncStateLoading<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@ -442,13 +413,13 @@ class _$AsyncStateLoading<T>
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties..add(DiagnosticsProperty('type', 'AsyncState<$T>.loading'));
|
properties.add(DiagnosticsProperty('type', 'AsyncState<$T>.loading'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType && other is AsyncStateLoading<T>);
|
(other.runtimeType == runtimeType && other is _$AsyncStateLoading<T>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -534,29 +505,29 @@ abstract class AsyncStateLoading<T> implements AsyncState<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract class $AsyncStateErrorCopyWith<T, $Res> {
|
abstract class _$$AsyncStateErrorCopyWith<T, $Res> {
|
||||||
factory $AsyncStateErrorCopyWith(
|
factory _$$AsyncStateErrorCopyWith(_$AsyncStateError<T> value,
|
||||||
AsyncStateError<T> value, $Res Function(AsyncStateError<T>) then) =
|
$Res Function(_$AsyncStateError<T>) then) =
|
||||||
_$AsyncStateErrorCopyWithImpl<T, $Res>;
|
__$$AsyncStateErrorCopyWithImpl<T, $Res>;
|
||||||
$Res call({String errorTerm});
|
$Res call({String errorTerm});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class _$AsyncStateErrorCopyWithImpl<T, $Res>
|
class __$$AsyncStateErrorCopyWithImpl<T, $Res>
|
||||||
extends _$AsyncStateCopyWithImpl<T, $Res>
|
extends _$AsyncStateCopyWithImpl<T, $Res>
|
||||||
implements $AsyncStateErrorCopyWith<T, $Res> {
|
implements _$$AsyncStateErrorCopyWith<T, $Res> {
|
||||||
_$AsyncStateErrorCopyWithImpl(
|
__$$AsyncStateErrorCopyWithImpl(
|
||||||
AsyncStateError<T> _value, $Res Function(AsyncStateError<T>) _then)
|
_$AsyncStateError<T> _value, $Res Function(_$AsyncStateError<T>) _then)
|
||||||
: super(_value, (v) => _then(v as AsyncStateError<T>));
|
: super(_value, (v) => _then(v as _$AsyncStateError<T>));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AsyncStateError<T> get _value => super._value as AsyncStateError<T>;
|
_$AsyncStateError<T> get _value => super._value as _$AsyncStateError<T>;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? errorTerm = freezed,
|
Object? errorTerm = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(AsyncStateError<T>(
|
return _then(_$AsyncStateError<T>(
|
||||||
errorTerm == freezed
|
errorTerm == freezed
|
||||||
? _value.errorTerm
|
? _value.errorTerm
|
||||||
: errorTerm // ignore: cast_nullable_to_non_nullable
|
: errorTerm // ignore: cast_nullable_to_non_nullable
|
||||||
|
@ -592,7 +563,7 @@ class _$AsyncStateError<T>
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is AsyncStateError<T> &&
|
other is _$AsyncStateError<T> &&
|
||||||
const DeepCollectionEquality().equals(other.errorTerm, errorTerm));
|
const DeepCollectionEquality().equals(other.errorTerm, errorTerm));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -602,8 +573,9 @@ class _$AsyncStateError<T>
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
$AsyncStateErrorCopyWith<T, AsyncStateError<T>> get copyWith =>
|
_$$AsyncStateErrorCopyWith<T, _$AsyncStateError<T>> get copyWith =>
|
||||||
_$AsyncStateErrorCopyWithImpl<T, AsyncStateError<T>>(this, _$identity);
|
__$$AsyncStateErrorCopyWithImpl<T, _$AsyncStateError<T>>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
|
@ -681,10 +653,10 @@ class _$AsyncStateError<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class AsyncStateError<T> implements AsyncState<T> {
|
abstract class AsyncStateError<T> implements AsyncState<T> {
|
||||||
const factory AsyncStateError(String errorTerm) = _$AsyncStateError<T>;
|
const factory AsyncStateError(final String errorTerm) = _$AsyncStateError<T>;
|
||||||
|
|
||||||
String get errorTerm;
|
String get errorTerm;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$AsyncStateErrorCopyWith<T, AsyncStateError<T>> get copyWith =>
|
_$$AsyncStateErrorCopyWith<T, _$AsyncStateError<T>> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ part of 'async_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$AsyncStore<T> on _AsyncStore<T>, Store {
|
mixin _$AsyncStore<T> on _AsyncStore<T>, Store {
|
||||||
Computed<bool>? _$isLoadingComputed;
|
Computed<bool>? _$isLoadingComputed;
|
||||||
|
@ -23,7 +23,8 @@ mixin _$AsyncStore<T> on _AsyncStore<T>, Store {
|
||||||
name: '_AsyncStore.errorTerm'))
|
name: '_AsyncStore.errorTerm'))
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
final _$asyncStateAtom = Atom(name: '_AsyncStore.asyncState');
|
late final _$asyncStateAtom =
|
||||||
|
Atom(name: '_AsyncStore.asyncState', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AsyncState<T> get asyncState {
|
AsyncState<T> get asyncState {
|
||||||
|
@ -38,14 +39,16 @@ mixin _$AsyncStore<T> on _AsyncStore<T>, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$runAsyncAction = AsyncAction('_AsyncStore.run');
|
late final _$runAsyncAction =
|
||||||
|
AsyncAction('_AsyncStore.run', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<T?> run(AsyncValueGetter<T> callback, {bool refresh = false}) {
|
Future<T?> run(AsyncValueGetter<T> callback, {bool refresh = false}) {
|
||||||
return _$runAsyncAction.run(() => super.run(callback, refresh: refresh));
|
return _$runAsyncAction.run(() => super.run(callback, refresh: refresh));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$runLemmyAsyncAction = AsyncAction('_AsyncStore.runLemmy');
|
late final _$runLemmyAsyncAction =
|
||||||
|
AsyncAction('_AsyncStore.runLemmy', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<T?> runLemmy(String instanceHost, LemmyApiQuery<T> query,
|
Future<T?> runLemmy(String instanceHost, LemmyApiQuery<T> query,
|
||||||
|
@ -54,7 +57,8 @@ mixin _$AsyncStore<T> on _AsyncStore<T>, Store {
|
||||||
.run(() => super.runLemmy(instanceHost, query, refresh: refresh));
|
.run(() => super.runLemmy(instanceHost, query, refresh: refresh));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_AsyncStoreActionController = ActionController(name: '_AsyncStore');
|
late final _$_AsyncStoreActionController =
|
||||||
|
ActionController(name: '_AsyncStore', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void setData(T data) {
|
void setData(T data) {
|
||||||
|
@ -67,6 +71,17 @@ mixin _$AsyncStore<T> on _AsyncStore<T>, Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {
|
||||||
|
final _$actionInfo =
|
||||||
|
_$_AsyncStoreActionController.startAction(name: '_AsyncStore.reset');
|
||||||
|
try {
|
||||||
|
return super.reset();
|
||||||
|
} finally {
|
||||||
|
_$_AsyncStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''
|
return '''
|
||||||
|
|
|
@ -12,32 +12,45 @@ class AsyncStoreListener<T> extends SingleChildStatelessWidget {
|
||||||
T data,
|
T data,
|
||||||
)? successMessageBuilder;
|
)? successMessageBuilder;
|
||||||
|
|
||||||
|
final void Function(
|
||||||
|
BuildContext context,
|
||||||
|
T data,
|
||||||
|
)? onSuccess;
|
||||||
|
|
||||||
const AsyncStoreListener({
|
const AsyncStoreListener({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.asyncStore,
|
required this.asyncStore,
|
||||||
this.successMessageBuilder,
|
this.successMessageBuilder,
|
||||||
Widget? child,
|
this.onSuccess,
|
||||||
}) : super(key: key, child: child);
|
super.child,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildWithChild(BuildContext context, Widget? child) {
|
Widget buildWithChild(BuildContext context, Widget? child) {
|
||||||
return ObserverListener<AsyncStore<T>>(
|
return ObserverListener<AsyncStore<T>>(
|
||||||
store: asyncStore,
|
store: asyncStore,
|
||||||
listener: (context, store) {
|
listener: (context, store) {
|
||||||
final errorTerm = store.errorTerm;
|
store.map(
|
||||||
|
loading: () {},
|
||||||
|
error: (errorTerm) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(SnackBar(content: Text(errorTerm.tr(context))));
|
||||||
|
},
|
||||||
|
data: (data) {
|
||||||
|
onSuccess?.call(context, data);
|
||||||
|
|
||||||
if (errorTerm != null) {
|
if (successMessageBuilder != null) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
..hideCurrentSnackBar()
|
..hideCurrentSnackBar()
|
||||||
..showSnackBar(SnackBar(content: Text(errorTerm.tr(context))));
|
..showSnackBar(
|
||||||
} else if (store.asyncState is AsyncStateData &&
|
SnackBar(
|
||||||
(successMessageBuilder != null)) {
|
content: Text(successMessageBuilder!(context, data)),
|
||||||
ScaffoldMessenger.of(context)
|
),
|
||||||
..hideCurrentSnackBar()
|
);
|
||||||
..showSnackBar(SnackBar(
|
}
|
||||||
content: Text(successMessageBuilder!(
|
},
|
||||||
context, (store.asyncState as AsyncStateData).data))));
|
);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: child ?? const SizedBox(),
|
child: child ?? const SizedBox(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -52,3 +52,14 @@ extension UserPreferredNames on PersonSafe {
|
||||||
extension CommentLink on Comment {
|
extension CommentLink on Comment {
|
||||||
String get link => 'https://$instanceHost/post/$postId/comment/$id';
|
String get link => 'https://$instanceHost/post/$postId/comment/$id';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inspired by https://github.com/LemmyNet/lemmy-ui/blob/66c846ededef8c0afd5aaadca4aaedcbaeab3ee6/src/shared/utils.ts#L533
|
||||||
|
extension PersonSafeCakeDay on PersonSafe {
|
||||||
|
bool get isCakeDay {
|
||||||
|
final now = DateTime.now().toUtc();
|
||||||
|
|
||||||
|
return now.day == published.day &&
|
||||||
|
now.month == published.month &&
|
||||||
|
now.year != published.year;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
|
||||||
|
|
||||||
// inspired by https://github.com/LemmyNet/lemmy-ui/blob/66c846ededef8c0afd5aaadca4aaedcbaeab3ee6/src/shared/utils.ts#L533
|
|
||||||
extension PersonSafeCakeDay on PersonSafe {
|
|
||||||
bool get isCakeDay {
|
|
||||||
final now = DateTime.now().toUtc();
|
|
||||||
|
|
||||||
return now.day == published.day &&
|
|
||||||
now.month == published.month &&
|
|
||||||
now.year != published.year;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import '../observer_consumers.dart';
|
||||||
|
|
||||||
|
extension BuildContextExtensions on BuildContext {
|
||||||
|
/// Get default [Jwt] for an instance
|
||||||
|
Jwt? defaultJwt(String instanceHost) =>
|
||||||
|
read<AccountsStore>().defaultUserDataFor(instanceHost)?.jwt;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
/// Picks a single image from the system
|
||||||
|
Future<XFile?> pickImage() async {
|
||||||
|
if (kIsWeb || Platform.isIOS || Platform.isAndroid) {
|
||||||
|
return ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
|
} else {
|
||||||
|
final result = await FilePicker.platform.pickFiles(type: FileType.image);
|
||||||
|
|
||||||
|
if (result == null) return null;
|
||||||
|
|
||||||
|
return XFile(result.files.single.path!);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../pages/community/community.dart';
|
import '../pages/community/community.dart';
|
||||||
import '../pages/full_post/full_post.dart';
|
import '../pages/full_post/full_post.dart';
|
||||||
import '../pages/instance.dart';
|
|
||||||
import '../pages/media_view.dart';
|
import '../pages/media_view.dart';
|
||||||
import '../pages/user.dart';
|
import '../pages/user.dart';
|
||||||
|
|
||||||
|
@ -25,9 +24,6 @@ Future<dynamic> goToReplace(
|
||||||
builder: builder,
|
builder: builder,
|
||||||
));
|
));
|
||||||
|
|
||||||
void goToInstance(BuildContext context, String instanceHost) =>
|
|
||||||
goTo(context, (context) => InstancePage(instanceHost: instanceHost));
|
|
||||||
|
|
||||||
// ignore: camel_case_types
|
// ignore: camel_case_types
|
||||||
abstract class goToCommunity {
|
abstract class goToCommunity {
|
||||||
/// Navigates to `CommunityPage`
|
/// Navigates to `CommunityPage`
|
||||||
|
@ -63,7 +59,6 @@ void goToMedia(BuildContext context, String url) => Navigator.push(
|
||||||
context,
|
context,
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
pageBuilder: (_, __, ___) => MediaViewPage(url),
|
pageBuilder: (_, __, ___) => MediaViewPage(url),
|
||||||
transitionDuration: const Duration(milliseconds: 300),
|
|
||||||
opaque: false,
|
opaque: false,
|
||||||
transitionsBuilder: (_, animation, __, child) =>
|
transitionsBuilder: (_, animation, __, child) =>
|
||||||
FadeTransition(opacity: animation, child: child),
|
FadeTransition(opacity: animation, child: child),
|
||||||
|
|
|
@ -8,34 +8,24 @@ import 'observer_consumers.dart';
|
||||||
/// Important: this will not make [context.watch] react to changes
|
/// Important: this will not make [context.watch] react to changes
|
||||||
class MobxProvider<T extends Store> extends Provider<T> {
|
class MobxProvider<T extends Store> extends Provider<T> {
|
||||||
MobxProvider({
|
MobxProvider({
|
||||||
Key? key,
|
super.key,
|
||||||
required Create<T> create,
|
required super.create,
|
||||||
bool? lazy,
|
super.lazy,
|
||||||
TransitionBuilder? builder,
|
super.builder,
|
||||||
Widget? child,
|
super.child,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
|
||||||
create: create,
|
|
||||||
dispose: (context, store) {
|
dispose: (context, store) {
|
||||||
if (store is DisposableStore) store.dispose();
|
if (store is DisposableStore) store.dispose();
|
||||||
},
|
},
|
||||||
lazy: lazy,
|
|
||||||
builder: builder,
|
|
||||||
child: child,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// will not dispose the store
|
/// will not dispose the store
|
||||||
MobxProvider.value({
|
MobxProvider.value({
|
||||||
Key? key,
|
super.key,
|
||||||
required T value,
|
required super.value,
|
||||||
TransitionBuilder? builder,
|
super.builder,
|
||||||
Widget? child,
|
super.child,
|
||||||
}) : super.value(
|
}) : super.value();
|
||||||
key: key,
|
|
||||||
builder: builder,
|
|
||||||
value: value,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// tracks reactions and disposes them in [DisposableStore.dispose]
|
/// tracks reactions and disposes them in [DisposableStore.dispose]
|
||||||
|
|
|
@ -14,10 +14,10 @@ class ObserverBuilder<T extends Store> extends StatelessWidget {
|
||||||
final MobxBuilder<T> builder;
|
final MobxBuilder<T> builder;
|
||||||
|
|
||||||
const ObserverBuilder({
|
const ObserverBuilder({
|
||||||
Key? key,
|
super.key,
|
||||||
this.store,
|
this.store,
|
||||||
required this.builder,
|
required this.builder,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -38,11 +38,11 @@ class ObserverListener<T extends Store> extends HookWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const ObserverListener({
|
const ObserverListener({
|
||||||
Key? key,
|
super.key,
|
||||||
this.store,
|
this.store,
|
||||||
required this.listener,
|
required this.listener,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -64,11 +64,11 @@ class ObserverConsumer<T extends Store> extends HookWidget {
|
||||||
final MobxBuilder<T> builder;
|
final MobxBuilder<T> builder;
|
||||||
|
|
||||||
const ObserverConsumer({
|
const ObserverConsumer({
|
||||||
Key? key,
|
super.key,
|
||||||
this.store,
|
this.store,
|
||||||
required this.listener,
|
required this.listener,
|
||||||
required this.builder,
|
required this.builder,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// utililty class for traversing through multiline text
|
||||||
|
class TextLinesIterator extends Iterator {
|
||||||
|
String text;
|
||||||
|
int beg;
|
||||||
|
int end;
|
||||||
|
TextSelection? selection;
|
||||||
|
|
||||||
|
TextLinesIterator(this.text, {this.selection})
|
||||||
|
: end = -1,
|
||||||
|
beg = -1;
|
||||||
|
|
||||||
|
TextLinesIterator.fromController(TextEditingController controller)
|
||||||
|
: this(controller.text, selection: controller.selection);
|
||||||
|
|
||||||
|
bool get isWithinSelection {
|
||||||
|
final selection = this.selection;
|
||||||
|
if (selection == null || beg == -1) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return (selection.end >= beg && beg >= selection.start) ||
|
||||||
|
(selection.end >= end && end >= selection.start) ||
|
||||||
|
(end >= selection.start && selection.start >= beg) ||
|
||||||
|
(end >= selection.end && selection.end >= beg) ||
|
||||||
|
(beg <= selection.start &&
|
||||||
|
selection.start <= end &&
|
||||||
|
beg <= selection.end &&
|
||||||
|
selection.end <= end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get current {
|
||||||
|
return text.substring(beg, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
set current(String newVal) {
|
||||||
|
final selected = isWithinSelection;
|
||||||
|
text = text.replaceRange(beg, end, newVal);
|
||||||
|
final wordLen = end - beg;
|
||||||
|
final dif = newVal.length - wordLen;
|
||||||
|
end += dif;
|
||||||
|
|
||||||
|
final selection = this.selection;
|
||||||
|
if (selection == null) return;
|
||||||
|
|
||||||
|
if (selected || selection.baseOffset > end) {
|
||||||
|
this.selection =
|
||||||
|
selection.copyWith(extentOffset: selection.extentOffset + dif);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
end = -1;
|
||||||
|
beg = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool moveNext() {
|
||||||
|
if (end == text.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (beg == -1) {
|
||||||
|
end = 0;
|
||||||
|
beg = 0;
|
||||||
|
} else {
|
||||||
|
end += 1;
|
||||||
|
beg = end;
|
||||||
|
}
|
||||||
|
for (; end < text.length; end++) {
|
||||||
|
if (text[end] == '\n') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end = text.length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the lines as a list but also moves the pointer to the back
|
||||||
|
List<String> get asList {
|
||||||
|
reset();
|
||||||
|
final list = <String>[];
|
||||||
|
while (moveNext()) {
|
||||||
|
list.add(current);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,8 @@ class AboutTile extends HookWidget {
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.code),
|
icon: const Icon(Icons.code),
|
||||||
label: const Text('source code'),
|
label: const Text('source code'),
|
||||||
onPressed: () => openInBrowser(lemmurRepositoryLink),
|
onPressed: () =>
|
||||||
|
launchLink(link: lemmurRepositoryUrl, context: context),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.monetization_on),
|
icon: const Icon(Icons.monetization_on),
|
||||||
|
@ -56,13 +57,17 @@ class AboutTile extends HookWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () =>
|
onPressed: () => launchLink(
|
||||||
openInBrowser('https://patreon.com/lemmur'),
|
link: patreonUrl,
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
child: const Text('Patreon'),
|
child: const Text('Patreon'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () =>
|
onPressed: () => launchLink(
|
||||||
openInBrowser('https://buymeacoff.ee/lemmur'),
|
link: buyMeACoffeeUrl,
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
child: const Text('Buy Me a Coffee'),
|
child: const Text('Buy Me a Coffee'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -91,7 +96,7 @@ class AboutTile extends HookWidget {
|
||||||
class ChangelogPage extends StatelessWidget {
|
class ChangelogPage extends StatelessWidget {
|
||||||
final String changelog;
|
final String changelog;
|
||||||
|
|
||||||
const ChangelogPage(this.changelog, {Key? key}) : super(key: key);
|
const ChangelogPage(this.changelog, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -10,14 +10,14 @@ import 'cached_network_image.dart';
|
||||||
/// Can be disabled with `noBlank`
|
/// Can be disabled with `noBlank`
|
||||||
class Avatar extends HookWidget {
|
class Avatar extends HookWidget {
|
||||||
const Avatar({
|
const Avatar({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.url,
|
required this.url,
|
||||||
this.radius = 25,
|
this.radius = 25,
|
||||||
this.noBlank = false,
|
this.noBlank = false,
|
||||||
this.alwaysShow = false,
|
this.alwaysShow = false,
|
||||||
this.padding = EdgeInsets.zero,
|
this.padding = EdgeInsets.zero,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
final String? url;
|
final String? url;
|
||||||
final double radius;
|
final double radius;
|
||||||
|
|
|
@ -41,8 +41,8 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
this.width,
|
this.width,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.cache = true,
|
this.cache = true,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import '../../l10n/l10n.dart';
|
||||||
import '../../stores/config_store.dart';
|
import '../../stores/config_store.dart';
|
||||||
import '../../util/async_store_listener.dart';
|
import '../../util/async_store_listener.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/extensions/cake_day.dart';
|
|
||||||
import '../../util/goto.dart';
|
import '../../util/goto.dart';
|
||||||
import '../../util/mobx_provider.dart';
|
import '../../util/mobx_provider.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
|
@ -36,8 +35,8 @@ class CommentWidget extends StatelessWidget {
|
||||||
this.canBeMarkedAsRead = false,
|
this.canBeMarkedAsRead = false,
|
||||||
this.hideOnRead = false,
|
this.hideOnRead = false,
|
||||||
this.userMentionId,
|
this.userMentionId,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
CommentWidget.fromCommentView(
|
CommentWidget.fromCommentView(
|
||||||
CommentView cv, {
|
CommentView cv, {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import 'comment_more_menu_button.dart';
|
||||||
import 'comment_store.dart';
|
import 'comment_store.dart';
|
||||||
|
|
||||||
class CommentActions extends HookWidget {
|
class CommentActions extends HookWidget {
|
||||||
const CommentActions({Key? key}) : super(key: key);
|
const CommentActions({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
|
||||||
|
|
||||||
import '../../hooks/logged_in_action.dart';
|
import '../../hooks/logged_in_action.dart';
|
||||||
import '../../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../url_launcher.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/icons.dart';
|
import '../../util/icons.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
|
@ -18,7 +18,7 @@ import 'comment.dart';
|
||||||
import 'comment_store.dart';
|
import 'comment_store.dart';
|
||||||
|
|
||||||
class CommentMoreMenuButton extends HookWidget {
|
class CommentMoreMenuButton extends HookWidget {
|
||||||
const CommentMoreMenuButton({Key? key}) : super(key: key);
|
const CommentMoreMenuButton({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -44,9 +44,8 @@ class _CommentMoreMenuPopup extends HookWidget {
|
||||||
final CommentStore store;
|
final CommentStore store;
|
||||||
|
|
||||||
const _CommentMoreMenuPopup({
|
const _CommentMoreMenuPopup({
|
||||||
Key? key,
|
|
||||||
required this.store,
|
required this.store,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -83,13 +82,7 @@ class _CommentMoreMenuPopup extends HookWidget {
|
||||||
leading: const Icon(Icons.open_in_browser),
|
leading: const Icon(Icons.open_in_browser),
|
||||||
title: const Text('Open in browser'),
|
title: const Text('Open in browser'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (await ul.canLaunch(comment.link)) {
|
await launchLink(link: comment.link, context: context);
|
||||||
await ul.launch(comment.link);
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text("can't open in browser")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,6 +54,7 @@ abstract class _CommentStore with Store {
|
||||||
_CommentStore(
|
_CommentStore(
|
||||||
this._accountsStore, {
|
this._accountsStore, {
|
||||||
required CommentTree commentTree,
|
required CommentTree commentTree,
|
||||||
|
// ignore: unused_element
|
||||||
this.userMentionId,
|
this.userMentionId,
|
||||||
required this.depth,
|
required this.depth,
|
||||||
required this.canBeMarkedAsRead,
|
required this.canBeMarkedAsRead,
|
||||||
|
|
|
@ -6,7 +6,7 @@ part of 'comment_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$CommentStore on _CommentStore, Store {
|
mixin _$CommentStore on _CommentStore, Store {
|
||||||
Computed<bool>? _$isMineComputed;
|
Computed<bool>? _$isMineComputed;
|
||||||
|
@ -28,7 +28,8 @@ mixin _$CommentStore on _CommentStore, Store {
|
||||||
Computed<bool>(() => super.isOP, name: '_CommentStore.isOP'))
|
Computed<bool>(() => super.isOP, name: '_CommentStore.isOP'))
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
final _$commentAtom = Atom(name: '_CommentStore.comment');
|
late final _$commentAtom =
|
||||||
|
Atom(name: '_CommentStore.comment', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CommentView get comment {
|
CommentView get comment {
|
||||||
|
@ -43,7 +44,8 @@ mixin _$CommentStore on _CommentStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$selectableAtom = Atom(name: '_CommentStore.selectable');
|
late final _$selectableAtom =
|
||||||
|
Atom(name: '_CommentStore.selectable', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get selectable {
|
bool get selectable {
|
||||||
|
@ -58,7 +60,8 @@ mixin _$CommentStore on _CommentStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$collapsedAtom = Atom(name: '_CommentStore.collapsed');
|
late final _$collapsedAtom =
|
||||||
|
Atom(name: '_CommentStore.collapsed', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get collapsed {
|
bool get collapsed {
|
||||||
|
@ -73,7 +76,8 @@ mixin _$CommentStore on _CommentStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$showRawAtom = Atom(name: '_CommentStore.showRaw');
|
late final _$showRawAtom =
|
||||||
|
Atom(name: '_CommentStore.showRaw', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get showRaw {
|
bool get showRaw {
|
||||||
|
@ -88,64 +92,72 @@ mixin _$CommentStore on _CommentStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$reportAsyncAction = AsyncAction('_CommentStore.report');
|
late final _$reportAsyncAction =
|
||||||
|
AsyncAction('_CommentStore.report', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> report(Jwt token, String reason) {
|
Future<void> report(Jwt token, String reason) {
|
||||||
return _$reportAsyncAction.run(() => super.report(token, reason));
|
return _$reportAsyncAction.run(() => super.report(token, reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$deleteAsyncAction = AsyncAction('_CommentStore.delete');
|
late final _$deleteAsyncAction =
|
||||||
|
AsyncAction('_CommentStore.delete', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> delete(Jwt token) {
|
Future<void> delete(Jwt token) {
|
||||||
return _$deleteAsyncAction.run(() => super.delete(token));
|
return _$deleteAsyncAction.run(() => super.delete(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$saveAsyncAction = AsyncAction('_CommentStore.save');
|
late final _$saveAsyncAction =
|
||||||
|
AsyncAction('_CommentStore.save', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> save(Jwt token) {
|
Future<void> save(Jwt token) {
|
||||||
return _$saveAsyncAction.run(() => super.save(token));
|
return _$saveAsyncAction.run(() => super.save(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$blockAsyncAction = AsyncAction('_CommentStore.block');
|
late final _$blockAsyncAction =
|
||||||
|
AsyncAction('_CommentStore.block', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> block(Jwt token) {
|
Future<void> block(Jwt token) {
|
||||||
return _$blockAsyncAction.run(() => super.block(token));
|
return _$blockAsyncAction.run(() => super.block(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$markAsReadAsyncAction = AsyncAction('_CommentStore.markAsRead');
|
late final _$markAsReadAsyncAction =
|
||||||
|
AsyncAction('_CommentStore.markAsRead', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> markAsRead(Jwt token) {
|
Future<void> markAsRead(Jwt token) {
|
||||||
return _$markAsReadAsyncAction.run(() => super.markAsRead(token));
|
return _$markAsReadAsyncAction.run(() => super.markAsRead(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_voteAsyncAction = AsyncAction('_CommentStore._vote');
|
late final _$_voteAsyncAction =
|
||||||
|
AsyncAction('_CommentStore._vote', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> _vote(VoteType voteType, Jwt token) {
|
Future<void> _vote(VoteType voteType, Jwt token) {
|
||||||
return _$_voteAsyncAction.run(() => super._vote(voteType, token));
|
return _$_voteAsyncAction.run(() => super._vote(voteType, token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$upVoteAsyncAction = AsyncAction('_CommentStore.upVote');
|
late final _$upVoteAsyncAction =
|
||||||
|
AsyncAction('_CommentStore.upVote', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> upVote(Jwt token) {
|
Future<void> upVote(Jwt token) {
|
||||||
return _$upVoteAsyncAction.run(() => super.upVote(token));
|
return _$upVoteAsyncAction.run(() => super.upVote(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$downVoteAsyncAction = AsyncAction('_CommentStore.downVote');
|
late final _$downVoteAsyncAction =
|
||||||
|
AsyncAction('_CommentStore.downVote', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> downVote(Jwt token) {
|
Future<void> downVote(Jwt token) {
|
||||||
return _$downVoteAsyncAction.run(() => super.downVote(token));
|
return _$downVoteAsyncAction.run(() => super.downVote(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_CommentStoreActionController =
|
late final _$_CommentStoreActionController =
|
||||||
ActionController(name: '_CommentStore');
|
ActionController(name: '_CommentStore', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void toggleShowRaw() {
|
void toggleShowRaw() {
|
||||||
|
|
|
@ -1,60 +1,87 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
import 'markdown_text.dart';
|
import '../../markdown_formatter.dart';
|
||||||
|
import '../markdown_text.dart';
|
||||||
|
|
||||||
|
export 'editor_toolbar.dart';
|
||||||
|
|
||||||
|
class EditorController {
|
||||||
|
final TextEditingController textEditingController;
|
||||||
|
final FocusNode focusNode;
|
||||||
|
final String instanceHost;
|
||||||
|
|
||||||
|
EditorController({
|
||||||
|
required this.textEditingController,
|
||||||
|
required this.focusNode,
|
||||||
|
required this.instanceHost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorController useEditorController({
|
||||||
|
required String instanceHost,
|
||||||
|
String? text,
|
||||||
|
}) {
|
||||||
|
final focusNode = useFocusNode();
|
||||||
|
final textEditingController = useTextEditingController(text: text);
|
||||||
|
return EditorController(
|
||||||
|
textEditingController: textEditingController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
instanceHost: instanceHost);
|
||||||
|
}
|
||||||
|
|
||||||
/// A text field with added functionality for ease of editing
|
/// A text field with added functionality for ease of editing
|
||||||
class Editor extends HookWidget {
|
class Editor extends HookWidget {
|
||||||
final TextEditingController? controller;
|
final EditorController controller;
|
||||||
final FocusNode? focusNode;
|
|
||||||
final ValueChanged<String>? onSubmitted;
|
final ValueChanged<String>? onSubmitted;
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
final int? minLines;
|
final int? minLines;
|
||||||
final int? maxLines;
|
final int? maxLines;
|
||||||
final String? labelText;
|
final String? labelText;
|
||||||
|
final String? initialValue;
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
|
||||||
/// Whether the editor should be preview the contents
|
/// Whether the editor should be preview the contents
|
||||||
final bool fancy;
|
final bool fancy;
|
||||||
final String instanceHost;
|
|
||||||
|
|
||||||
const Editor({
|
const Editor({
|
||||||
Key? key,
|
super.key,
|
||||||
this.controller,
|
required this.controller,
|
||||||
this.focusNode,
|
|
||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
|
this.onChanged,
|
||||||
this.minLines = 5,
|
this.minLines = 5,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.labelText,
|
this.labelText,
|
||||||
|
this.initialValue,
|
||||||
this.fancy = false,
|
this.fancy = false,
|
||||||
required this.instanceHost,
|
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final defaultController = useTextEditingController();
|
|
||||||
final actualController = controller ?? defaultController;
|
|
||||||
|
|
||||||
if (fancy) {
|
if (fancy) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: MarkdownText(
|
child: MarkdownText(
|
||||||
actualController.text,
|
controller.textEditingController.text,
|
||||||
instanceHost: instanceHost,
|
instanceHost: controller.instanceHost,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: actualController,
|
focusNode: controller.focusNode,
|
||||||
focusNode: focusNode,
|
controller: controller.textEditingController,
|
||||||
autofocus: autofocus,
|
autofocus: autofocus,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
onChanged: onChanged,
|
||||||
onSubmitted: onSubmitted,
|
onSubmitted: onSubmitted,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
minLines: minLines,
|
minLines: minLines,
|
||||||
decoration: InputDecoration(labelText: labelText),
|
decoration: InputDecoration(labelText: labelText),
|
||||||
|
inputFormatters: [MarkdownFormatter()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../../util/extensions/api.dart';
|
||||||
|
import '../../../widgets/avatar.dart';
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import 'editor_toolbar_store.dart';
|
||||||
|
|
||||||
|
class PickPersonDialog extends StatelessWidget {
|
||||||
|
final EditorToolbarStore store;
|
||||||
|
|
||||||
|
const PickPersonDialog._(this.store);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userData =
|
||||||
|
context.read<AccountsStore>().defaultUserDataFor(store.instanceHost);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(L10n.of(context).select_user),
|
||||||
|
content: TypeAheadField<PersonViewSafe>(
|
||||||
|
suggestionsCallback: (pattern) async {
|
||||||
|
if (pattern.trim().isEmpty) return const Iterable.empty();
|
||||||
|
return LemmyApiV3(store.instanceHost)
|
||||||
|
.run(Search(
|
||||||
|
q: pattern,
|
||||||
|
auth: userData?.jwt.raw,
|
||||||
|
type: SearchType.users,
|
||||||
|
limit: 10,
|
||||||
|
))
|
||||||
|
.then((value) => value.users);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, user) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Avatar(
|
||||||
|
url: user.person.avatar,
|
||||||
|
radius: 20,
|
||||||
|
),
|
||||||
|
title: Text(user.person.originPreferredName),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuggestionSelected: (suggestion) =>
|
||||||
|
Navigator.of(context).pop(suggestion),
|
||||||
|
loadingBuilder: (context) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
keepSuggestionsOnLoading: false,
|
||||||
|
noItemsFoundBuilder: (context) => const SizedBox(),
|
||||||
|
hideOnEmpty: true,
|
||||||
|
textFieldConfiguration: const TextFieldConfiguration(autofocus: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<PersonViewSafe?> show(BuildContext context) async {
|
||||||
|
final store = context.read<EditorToolbarStore>();
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PickPersonDialog._(store),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PickCommunityDialog extends StatelessWidget {
|
||||||
|
final EditorToolbarStore store;
|
||||||
|
|
||||||
|
const PickCommunityDialog._(this.store);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userData =
|
||||||
|
context.read<AccountsStore>().defaultUserDataFor(store.instanceHost);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(L10n.of(context).select_community),
|
||||||
|
content: TypeAheadField<CommunityView>(
|
||||||
|
suggestionsCallback: (pattern) async {
|
||||||
|
if (pattern.trim().isEmpty) return const Iterable.empty();
|
||||||
|
return LemmyApiV3(store.instanceHost)
|
||||||
|
.run(Search(
|
||||||
|
q: pattern,
|
||||||
|
auth: userData?.jwt.raw,
|
||||||
|
type: SearchType.communities,
|
||||||
|
limit: 10,
|
||||||
|
))
|
||||||
|
.then((value) => value.communities);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, community) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Avatar(
|
||||||
|
url: community.community.icon,
|
||||||
|
radius: 20,
|
||||||
|
),
|
||||||
|
title: Text(community.community.originPreferredName),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuggestionSelected: (suggestion) =>
|
||||||
|
Navigator.of(context).pop(suggestion),
|
||||||
|
loadingBuilder: (context) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
keepSuggestionsOnLoading: false,
|
||||||
|
noItemsFoundBuilder: (context) => const SizedBox(),
|
||||||
|
hideOnEmpty: true,
|
||||||
|
textFieldConfiguration: const TextFieldConfiguration(autofocus: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<CommunityView?> show(BuildContext context) async {
|
||||||
|
final store = context.read<EditorToolbarStore>();
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PickCommunityDialog._(store),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,504 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../markdown_formatter.dart';
|
||||||
|
import '../../resources/links.dart';
|
||||||
|
import '../../url_launcher.dart';
|
||||||
|
import '../../util/async_store_listener.dart';
|
||||||
|
import '../../util/extensions/api.dart';
|
||||||
|
import '../../util/extensions/spaced.dart';
|
||||||
|
import '../../util/files.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../util/text_lines_iterator.dart';
|
||||||
|
import 'editor.dart';
|
||||||
|
import 'editor_picking_dialog.dart';
|
||||||
|
import 'editor_toolbar_store.dart';
|
||||||
|
|
||||||
|
class _Reformat {
|
||||||
|
final String text;
|
||||||
|
final int selectionBeginningShift;
|
||||||
|
final int selectionEndingShift;
|
||||||
|
_Reformat({
|
||||||
|
required this.text,
|
||||||
|
this.selectionBeginningShift = 0,
|
||||||
|
this.selectionEndingShift = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HeaderLevel {
|
||||||
|
h1(1),
|
||||||
|
h2(2),
|
||||||
|
h3(3),
|
||||||
|
h4(4),
|
||||||
|
h5(5),
|
||||||
|
h6(6);
|
||||||
|
|
||||||
|
const HeaderLevel(this.value);
|
||||||
|
final int value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditorToolbar extends HookWidget {
|
||||||
|
final EditorController controller;
|
||||||
|
|
||||||
|
static const _height = 50.0;
|
||||||
|
|
||||||
|
const EditorToolbar(this.controller);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final visible = useListenable(controller.focusNode).hasFocus;
|
||||||
|
|
||||||
|
return MobxProvider(
|
||||||
|
create: (context) => EditorToolbarStore(controller.instanceHost),
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
return AsyncStoreListener(
|
||||||
|
asyncStore: context.read<EditorToolbarStore>().imageUploadState,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: kThemeAnimationDuration,
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
final offsetAnimation =
|
||||||
|
Tween<Offset>(begin: const Offset(0, 1.5), end: Offset.zero)
|
||||||
|
.animate(animation);
|
||||||
|
|
||||||
|
return SlideTransition(
|
||||||
|
position: offsetAnimation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: visible
|
||||||
|
? Material(
|
||||||
|
color: Theme.of(context).canvasColor,
|
||||||
|
child: SafeArea(
|
||||||
|
child: SizedBox(
|
||||||
|
height: _height,
|
||||||
|
width: double.infinity,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: _ToolbarBody(
|
||||||
|
controller: controller.textEditingController,
|
||||||
|
instanceHost: controller.instanceHost,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const safeArea = SizedBox(height: _height);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomSticky extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
const BottomSticky({required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToolbarBody extends HookWidget {
|
||||||
|
const _ToolbarBody({
|
||||||
|
required this.controller,
|
||||||
|
required this.instanceHost,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String instanceHost;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final loggedInAction = useLoggedInAction(instanceHost);
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.surround(
|
||||||
|
before: '**',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder),
|
||||||
|
icon: const Icon(Icons.format_bold),
|
||||||
|
tooltip: L10n.of(context).editor_bold,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.surround(
|
||||||
|
before: '*',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder),
|
||||||
|
icon: const Icon(Icons.format_italic),
|
||||||
|
tooltip: L10n.of(context).editor_italics,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final r =
|
||||||
|
await AddLinkDialog.show(context, controller.selectionText);
|
||||||
|
if (r != null) controller.reformat((_) => r);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.link),
|
||||||
|
tooltip: L10n.of(context).editor_link,
|
||||||
|
),
|
||||||
|
// Insert image
|
||||||
|
ObserverBuilder<EditorToolbarStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: loggedInAction((token) async {
|
||||||
|
if (store.imageUploadState.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final pic = await pickImage();
|
||||||
|
// pic is null when the picker was cancelled
|
||||||
|
|
||||||
|
if (pic != null) {
|
||||||
|
final picUrl = await store.uploadImage(pic.path, token);
|
||||||
|
|
||||||
|
if (picUrl != null) {
|
||||||
|
controller.reformatSimple('![]($picUrl)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on Exception catch (_) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text(L10n.of(context).failed_to_upload_image)));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
icon: store.imageUploadState.isLoading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: const Icon(Icons.image),
|
||||||
|
tooltip: L10n.of(context).editor_image,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final person = await PickPersonDialog.show(context);
|
||||||
|
|
||||||
|
if (person != null) {
|
||||||
|
final name =
|
||||||
|
'@${person.person.name}@${person.person.originInstanceHost}';
|
||||||
|
final link = person.person.actorId;
|
||||||
|
|
||||||
|
controller.reformatSimple('[$name]($link)');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.person),
|
||||||
|
tooltip: L10n.of(context).editor_user,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final community = await PickCommunityDialog.show(context);
|
||||||
|
if (community != null) {
|
||||||
|
final name =
|
||||||
|
'!${community.community.name}@${community.community.originInstanceHost}';
|
||||||
|
final link = community.community.actorId;
|
||||||
|
|
||||||
|
controller.reformatSimple('[$name]($link)');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.home),
|
||||||
|
tooltip: L10n.of(context).editor_community,
|
||||||
|
),
|
||||||
|
PopupMenuButton<HeaderLevel>(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
for (final h in HeaderLevel.values)
|
||||||
|
PopupMenuItem(
|
||||||
|
value: h,
|
||||||
|
child: Text(h.name.toUpperCase()),
|
||||||
|
onTap: () {
|
||||||
|
final header = '${'#' * h.value} ';
|
||||||
|
|
||||||
|
if (!controller.firstSelectedLine.startsWith(header)) {
|
||||||
|
controller.insertAtBeginningOfFirstSelectedLine(header);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tooltip: L10n.of(context).editor_header,
|
||||||
|
child: const Icon(Icons.h_mobiledata),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.surround(
|
||||||
|
before: '~~',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.format_strikethrough),
|
||||||
|
tooltip: L10n.of(context).editor_strikethrough,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.insertAtBeginningOfEverySelectedLine('> ');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.format_quote),
|
||||||
|
tooltip: L10n.of(context).editor_quote,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
final line = controller.firstSelectedLine;
|
||||||
|
|
||||||
|
// if theres a list in place, remove it
|
||||||
|
final listRemoved = () {
|
||||||
|
for (final c in unorderedListTypes) {
|
||||||
|
if (line.startsWith('$c ')) {
|
||||||
|
controller.removeAtBeginningOfEverySelectedLine('$c ');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
|
||||||
|
// if no list, then let's add one
|
||||||
|
if (!listRemoved) {
|
||||||
|
controller.insertAtBeginningOfEverySelectedLine(
|
||||||
|
'${unorderedListTypes.last} ');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.format_list_bulleted),
|
||||||
|
tooltip: L10n.of(context).editor_list,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.surround(
|
||||||
|
before: '`',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.code),
|
||||||
|
tooltip: L10n.of(context).editor_code,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.surround(
|
||||||
|
before: '~',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.subscript),
|
||||||
|
tooltip: L10n.of(context).editor_subscript,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.surround(
|
||||||
|
before: '^',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.superscript),
|
||||||
|
tooltip: L10n.of(context).editor_superscript,
|
||||||
|
),
|
||||||
|
//spoiler
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.reformat((selection) {
|
||||||
|
const textBeg = '\n::: spoiler spoiler\n';
|
||||||
|
final textMid = selection.isNotEmpty ? selection : '___';
|
||||||
|
const textEnd = '\n:::\n';
|
||||||
|
|
||||||
|
return _Reformat(
|
||||||
|
text: textBeg + textMid + textEnd,
|
||||||
|
selectionBeginningShift: textBeg.length,
|
||||||
|
selectionEndingShift:
|
||||||
|
textBeg.length + textMid.length - selection.length,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.warning),
|
||||||
|
tooltip: L10n.of(context).editor_spoiler,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
launchLink(link: markdownGuide, context: context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.question_mark),
|
||||||
|
tooltip: L10n.of(context).editor_help,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddLinkDialog extends HookWidget {
|
||||||
|
final String label;
|
||||||
|
final String url;
|
||||||
|
final String selection;
|
||||||
|
|
||||||
|
static final _websiteRegex = RegExp(r'https?:\/\/', caseSensitive: false);
|
||||||
|
|
||||||
|
AddLinkDialog(this.selection)
|
||||||
|
: label = selection.startsWith(_websiteRegex) ? '' : selection,
|
||||||
|
url = selection.startsWith(_websiteRegex) ? selection : '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final labelController = useTextEditingController(text: label);
|
||||||
|
final urlController = useTextEditingController(text: url);
|
||||||
|
|
||||||
|
void submit() {
|
||||||
|
final link = () {
|
||||||
|
if (urlController.text.startsWith(RegExp('https?://'))) {
|
||||||
|
return urlController.text;
|
||||||
|
} else {
|
||||||
|
return 'https://${urlController.text}';
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
final finalString = '[${labelController.text}]($link)';
|
||||||
|
Navigator.of(context).pop(_Reformat(
|
||||||
|
text: finalString,
|
||||||
|
selectionBeginningShift: finalString.length,
|
||||||
|
selectionEndingShift: finalString.length - selection.length,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(L10n.of(context).add_link),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: labelController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: L10n.of(context).editor_add_link_label),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: urlController,
|
||||||
|
decoration: const InputDecoration(hintText: 'https://example.com'),
|
||||||
|
onEditingComplete: submit,
|
||||||
|
autocorrect: false,
|
||||||
|
),
|
||||||
|
].spaced(10),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(L10n.of(context).cancel)),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: submit,
|
||||||
|
child: Text(L10n.of(context).add_link),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<_Reformat?> show(BuildContext context, String selection) async {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AddLinkDialog(selection),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on TextEditingController {
|
||||||
|
String get selectionText =>
|
||||||
|
text.substring(selection.baseOffset, selection.extentOffset);
|
||||||
|
String get beforeSelectionText => text.substring(0, selection.baseOffset);
|
||||||
|
String get afterSelectionText => text.substring(selection.extentOffset);
|
||||||
|
|
||||||
|
/// surrounds selection with given strings. If nothing is selected, placeholder is used in the middle
|
||||||
|
void surround({
|
||||||
|
required String before,
|
||||||
|
required String placeholder,
|
||||||
|
|
||||||
|
/// after = before if null
|
||||||
|
String? after,
|
||||||
|
}) {
|
||||||
|
after ??= before;
|
||||||
|
final beg = text.substring(0, selection.baseOffset);
|
||||||
|
final mid = () {
|
||||||
|
final m = text.substring(selection.baseOffset, selection.extentOffset);
|
||||||
|
if (m.isEmpty) return placeholder;
|
||||||
|
return m;
|
||||||
|
}();
|
||||||
|
final end = text.substring(selection.extentOffset);
|
||||||
|
|
||||||
|
value = value.copyWith(
|
||||||
|
text: '$beg$before$mid$after$end',
|
||||||
|
selection: selection.copyWith(
|
||||||
|
baseOffset: selection.baseOffset + before.length,
|
||||||
|
extentOffset: selection.baseOffset + before.length + mid.length,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
String get firstSelectedLine {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
final val = text.substring(text.getBeginningOfTheLine(selection.start - 1),
|
||||||
|
text.getEndOfTheLine(selection.end) - 1);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
void insertAtBeginningOfFirstSelectedLine(String s) {
|
||||||
|
final lines = TextLinesIterator.fromController(this)..moveNext();
|
||||||
|
lines.current = s + lines.current;
|
||||||
|
value = value.copyWith(
|
||||||
|
text: lines.text,
|
||||||
|
selection: selection.copyWith(
|
||||||
|
baseOffset: selection.baseOffset + s.length,
|
||||||
|
extentOffset: selection.extentOffset + s.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAtBeginningOfEverySelectedLine(String s) {
|
||||||
|
final lines = TextLinesIterator.fromController(this);
|
||||||
|
var linesCount = 0;
|
||||||
|
while (lines.moveNext()) {
|
||||||
|
if (lines.isWithinSelection) {
|
||||||
|
if (lines.current.startsWith(s)) {
|
||||||
|
lines.current = lines.current.substring(s.length);
|
||||||
|
linesCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.copyWith(
|
||||||
|
text: lines.text,
|
||||||
|
selection: selection.copyWith(
|
||||||
|
baseOffset: selection.baseOffset - s.length,
|
||||||
|
extentOffset: selection.extentOffset - s.length * linesCount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void insertAtBeginningOfEverySelectedLine(String s) {
|
||||||
|
final lines = TextLinesIterator.fromController(this);
|
||||||
|
var linesCount = 0;
|
||||||
|
while (lines.moveNext()) {
|
||||||
|
if (lines.isWithinSelection) {
|
||||||
|
if (!lines.current.startsWith(s)) {
|
||||||
|
lines.current = '$s${lines.current}';
|
||||||
|
linesCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.copyWith(
|
||||||
|
text: lines.text,
|
||||||
|
selection: selection.copyWith(
|
||||||
|
baseOffset: selection.baseOffset + s.length,
|
||||||
|
extentOffset: selection.extentOffset + s.length * linesCount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reformat(_Reformat Function(String selection) reformatter) {
|
||||||
|
final beg = beforeSelectionText;
|
||||||
|
final mid = selectionText;
|
||||||
|
final end = afterSelectionText;
|
||||||
|
|
||||||
|
final r = reformatter(mid);
|
||||||
|
value = value.copyWith(
|
||||||
|
text: '$beg${r.text}$end',
|
||||||
|
selection: selection.copyWith(
|
||||||
|
baseOffset: selection.baseOffset + r.selectionBeginningShift,
|
||||||
|
extentOffset: selection.extentOffset + r.selectionEndingShift,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reformatSimple(String text) =>
|
||||||
|
reformat((selection) => _Reformat(text: text));
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import 'package:lemmy_api_client/pictrs.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../util/async_store.dart';
|
||||||
|
import '../../util/pictrs.dart';
|
||||||
|
|
||||||
|
part 'editor_toolbar_store.g.dart';
|
||||||
|
|
||||||
|
class EditorToolbarStore = _EditorToolbarStore with _$EditorToolbarStore;
|
||||||
|
|
||||||
|
abstract class _EditorToolbarStore with Store {
|
||||||
|
final String instanceHost;
|
||||||
|
|
||||||
|
_EditorToolbarStore(this.instanceHost);
|
||||||
|
|
||||||
|
@observable
|
||||||
|
String? url;
|
||||||
|
|
||||||
|
final imageUploadState = AsyncStore<PictrsUploadFile>();
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get hasUploadedImage => imageUploadState.map(
|
||||||
|
loading: () => false,
|
||||||
|
error: (_) => false,
|
||||||
|
data: (_) => true,
|
||||||
|
);
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<String?> uploadImage(String filePath, Jwt token) async {
|
||||||
|
final instanceHost = this.instanceHost;
|
||||||
|
|
||||||
|
final upload = await imageUploadState.run(
|
||||||
|
() => PictrsApi(instanceHost)
|
||||||
|
.upload(
|
||||||
|
filePath: filePath,
|
||||||
|
auth: token.raw,
|
||||||
|
)
|
||||||
|
.then((value) => value.files.single),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (upload != null) {
|
||||||
|
final url = pathToPictrs(instanceHost, upload.file);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void removeImage() {
|
||||||
|
final pictrsFile = imageUploadState.map<PictrsUploadFile?>(
|
||||||
|
data: (data) => data,
|
||||||
|
loading: () => null,
|
||||||
|
error: (_) => null,
|
||||||
|
);
|
||||||
|
if (pictrsFile == null) return;
|
||||||
|
|
||||||
|
PictrsApi(instanceHost).delete(pictrsFile).catchError((_) {});
|
||||||
|
|
||||||
|
imageUploadState.reset();
|
||||||
|
url = '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'editor_toolbar_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
mixin _$EditorToolbarStore on _EditorToolbarStore, Store {
|
||||||
|
Computed<bool>? _$hasUploadedImageComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasUploadedImage => (_$hasUploadedImageComputed ??= Computed<bool>(
|
||||||
|
() => super.hasUploadedImage,
|
||||||
|
name: '_EditorToolbarStore.hasUploadedImage'))
|
||||||
|
.value;
|
||||||
|
|
||||||
|
late final _$urlAtom =
|
||||||
|
Atom(name: '_EditorToolbarStore.url', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get url {
|
||||||
|
_$urlAtom.reportRead();
|
||||||
|
return super.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set url(String? value) {
|
||||||
|
_$urlAtom.reportWrite(value, super.url, () {
|
||||||
|
super.url = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$uploadImageAsyncAction =
|
||||||
|
AsyncAction('_EditorToolbarStore.uploadImage', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> uploadImage(String filePath, Jwt token) {
|
||||||
|
return _$uploadImageAsyncAction
|
||||||
|
.run(() => super.uploadImage(filePath, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _$_EditorToolbarStoreActionController =
|
||||||
|
ActionController(name: '_EditorToolbarStore', context: context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void removeImage() {
|
||||||
|
final _$actionInfo = _$_EditorToolbarStoreActionController.startAction(
|
||||||
|
name: '_EditorToolbarStore.removeImage');
|
||||||
|
try {
|
||||||
|
return super.removeImage();
|
||||||
|
} finally {
|
||||||
|
_$_EditorToolbarStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
url: ${url},
|
||||||
|
hasUploadedImage: ${hasUploadedImage}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,10 +9,10 @@ class FullscreenableImage extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const FullscreenableImage({
|
const FullscreenableImage({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => InkWell(
|
Widget build(BuildContext context) => InkWell(
|
||||||
|
|
|
@ -17,6 +17,8 @@ class InfiniteScrollController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef Fetcher<T> = Future<List<T>> Function(int page, int batchSize);
|
||||||
|
|
||||||
/// `ListView.builder` with asynchronous data fetching and no `itemCount`
|
/// `ListView.builder` with asynchronous data fetching and no `itemCount`
|
||||||
class InfiniteScroll<T> extends HookWidget {
|
class InfiniteScroll<T> extends HookWidget {
|
||||||
/// How many items should be fetched per call
|
/// How many items should be fetched per call
|
||||||
|
@ -31,7 +33,7 @@ class InfiniteScroll<T> extends HookWidget {
|
||||||
/// Fetches data to be displayed. It is important to respect `batchSize`,
|
/// Fetches data to be displayed. It is important to respect `batchSize`,
|
||||||
/// if the returned list has less than `batchSize` then the InfiniteScroll
|
/// if the returned list has less than `batchSize` then the InfiniteScroll
|
||||||
/// is considered finished
|
/// is considered finished
|
||||||
final Future<List<T>> Function(int page, int batchSize) fetcher;
|
final Fetcher<T> fetcher;
|
||||||
|
|
||||||
final InfiniteScrollController? controller;
|
final InfiniteScrollController? controller;
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
import '../../pages/community/community.dart';
|
import '../../pages/community/community.dart';
|
||||||
|
import '../../pages/instance/instance.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/goto.dart';
|
import '../../util/goto.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
|
@ -67,9 +68,10 @@ class PostInfoSection extends StatelessWidget {
|
||||||
text: post.post.originInstanceHost,
|
text: post.post.originInstanceHost,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () => goToInstance(
|
..onTap = () => Navigator.of(context).push(
|
||||||
context,
|
InstancePage.route(
|
||||||
post.post.originInstanceHost,
|
post.post.originInstanceHost,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
|
||||||
|
|
||||||
import '../../hooks/logged_in_action.dart';
|
import '../../hooks/logged_in_action.dart';
|
||||||
import '../../pages/create_post.dart';
|
import '../../pages/create_post/create_post.dart';
|
||||||
import '../../pages/full_post/full_post_store.dart';
|
import '../../pages/full_post/full_post_store.dart';
|
||||||
import '../../stores/accounts_store.dart';
|
import '../../stores/accounts_store.dart';
|
||||||
|
import '../../url_launcher.dart';
|
||||||
import '../../util/icons.dart';
|
import '../../util/icons.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
import '../bottom_modal.dart';
|
import '../bottom_modal.dart';
|
||||||
|
@ -72,10 +72,7 @@ class PostMoreMenu extends HookWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.open_in_browser),
|
leading: const Icon(Icons.open_in_browser),
|
||||||
title: const Text('Open in browser'),
|
title: const Text('Open in browser'),
|
||||||
onTap: () async => await ul.canLaunch(post.post.apId)
|
onTap: () => launchLink(link: post.post.apId, context: context),
|
||||||
? ul.launch(post.post.apId)
|
|
||||||
: ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text("can't open in browser"))),
|
|
||||||
),
|
),
|
||||||
if (isMine)
|
if (isMine)
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
|
@ -6,7 +6,7 @@ part of 'post_store.dart';
|
||||||
// StoreGenerator
|
// StoreGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
mixin _$PostStore on _PostStore, Store {
|
mixin _$PostStore on _PostStore, Store {
|
||||||
Computed<String?>? _$urlDomainComputed;
|
Computed<String?>? _$urlDomainComputed;
|
||||||
|
@ -23,7 +23,8 @@ mixin _$PostStore on _PostStore, Store {
|
||||||
Computed<bool>(() => super.hasMedia, name: '_PostStore.hasMedia'))
|
Computed<bool>(() => super.hasMedia, name: '_PostStore.hasMedia'))
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
final _$postViewAtom = Atom(name: '_PostStore.postView');
|
late final _$postViewAtom =
|
||||||
|
Atom(name: '_PostStore.postView', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PostView get postView {
|
PostView get postView {
|
||||||
|
@ -38,35 +39,40 @@ mixin _$PostStore on _PostStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$saveAsyncAction = AsyncAction('_PostStore.save');
|
late final _$saveAsyncAction =
|
||||||
|
AsyncAction('_PostStore.save', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> save(Jwt token) {
|
Future<void> save(Jwt token) {
|
||||||
return _$saveAsyncAction.run(() => super.save(token));
|
return _$saveAsyncAction.run(() => super.save(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$reportAsyncAction = AsyncAction('_PostStore.report');
|
late final _$reportAsyncAction =
|
||||||
|
AsyncAction('_PostStore.report', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> report(Jwt token, String reason) {
|
Future<void> report(Jwt token, String reason) {
|
||||||
return _$reportAsyncAction.run(() => super.report(token, reason));
|
return _$reportAsyncAction.run(() => super.report(token, reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$blockUserAsyncAction = AsyncAction('_PostStore.blockUser');
|
late final _$blockUserAsyncAction =
|
||||||
|
AsyncAction('_PostStore.blockUser', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> blockUser(Jwt token) {
|
Future<void> blockUser(Jwt token) {
|
||||||
return _$blockUserAsyncAction.run(() => super.blockUser(token));
|
return _$blockUserAsyncAction.run(() => super.blockUser(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_voteAsyncAction = AsyncAction('_PostStore._vote');
|
late final _$_voteAsyncAction =
|
||||||
|
AsyncAction('_PostStore._vote', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> _vote(Jwt token, VoteType voteType) {
|
Future<void> _vote(Jwt token, VoteType voteType) {
|
||||||
return _$_voteAsyncAction.run(() => super._vote(token, voteType));
|
return _$_voteAsyncAction.run(() => super._vote(token, voteType));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _$_PostStoreActionController = ActionController(name: '_PostStore');
|
late final _$_PostStoreActionController =
|
||||||
|
ActionController(name: '_PostStore', context: context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updatePostView(PostView postView) {
|
void updatePostView(PostView postView) {
|
||||||
|
|
|
@ -6,10 +6,10 @@ class PullToRefresh extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const PullToRefresh({
|
const PullToRefresh({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.onRefresh,
|
required this.onRefresh,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -20,7 +20,7 @@ class RadioPicker<T> extends StatelessWidget {
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
|
|
||||||
const RadioPicker({
|
const RadioPicker({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.values,
|
required this.values,
|
||||||
required this.groupValue,
|
required this.groupValue,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
|
@ -28,7 +28,7 @@ class RadioPicker<T> extends StatelessWidget {
|
||||||
this.buttonBuilder,
|
this.buttonBuilder,
|
||||||
this.title,
|
this.title,
|
||||||
this.trailing,
|
this.trailing,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -70,8 +70,8 @@ class SortableInfiniteList<T> extends HookWidget {
|
||||||
|
|
||||||
class InfinitePostList extends SortableInfiniteList<PostView> {
|
class InfinitePostList extends SortableInfiniteList<PostView> {
|
||||||
InfinitePostList({
|
InfinitePostList({
|
||||||
required FetcherWithSorting<PostView> fetcher,
|
required super.fetcher,
|
||||||
InfiniteScrollController? controller,
|
super.controller,
|
||||||
}) : super(
|
}) : super(
|
||||||
itemBuilder: (post) => Column(
|
itemBuilder: (post) => Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -79,8 +79,6 @@ class InfinitePostList extends SortableInfiniteList<PostView> {
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
fetcher: fetcher,
|
|
||||||
controller: controller,
|
|
||||||
noItems: const Text('there are no posts'),
|
noItems: const Text('there are no posts'),
|
||||||
uniqueProp: (item) => item.post.apId,
|
uniqueProp: (item) => item.post.apId,
|
||||||
);
|
);
|
||||||
|
@ -88,15 +86,13 @@ class InfinitePostList extends SortableInfiniteList<PostView> {
|
||||||
|
|
||||||
class InfiniteCommentList extends SortableInfiniteList<CommentView> {
|
class InfiniteCommentList extends SortableInfiniteList<CommentView> {
|
||||||
InfiniteCommentList({
|
InfiniteCommentList({
|
||||||
required FetcherWithSorting<CommentView> fetcher,
|
required super.fetcher,
|
||||||
InfiniteScrollController? controller,
|
super.controller,
|
||||||
}) : super(
|
}) : super(
|
||||||
itemBuilder: (comment) => CommentWidget(
|
itemBuilder: (comment) => CommentWidget(
|
||||||
CommentTree(comment),
|
CommentTree(comment),
|
||||||
detached: true,
|
detached: true,
|
||||||
),
|
),
|
||||||
fetcher: fetcher,
|
|
||||||
controller: controller,
|
|
||||||
noItems: const Text('there are no comments'),
|
noItems: const Text('there are no comments'),
|
||||||
uniqueProp: (item) => item.comment.apId,
|
uniqueProp: (item) => item.comment.apId,
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,14 +14,14 @@ class TileAction extends StatelessWidget {
|
||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
|
|
||||||
const TileAction({
|
const TileAction({
|
||||||
Key? key,
|
super.key,
|
||||||
this.delayedLoading,
|
this.delayedLoading,
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
required this.tooltip,
|
required this.tooltip,
|
||||||
this.loading = false,
|
this.loading = false,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => IconButton(
|
Widget build(BuildContext context) => IconButton(
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue