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:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: subosito/flutter-action@v1
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
|
||||
|
@ -91,10 +91,6 @@ jobs:
|
|||
sudo apt-get update -y
|
||||
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
|
||||
run: |
|
||||
flutter build linux --release --target lib/main_prod.dart
|
||||
|
@ -110,10 +106,6 @@ jobs:
|
|||
with:
|
||||
channel: "stable"
|
||||
|
||||
- name: Enable windows support
|
||||
run: |
|
||||
flutter config --enable-windows-desktop
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
flutter build windows --release --target lib/main_prod.dart
|
||||
|
|
|
@ -90,10 +90,6 @@ jobs:
|
|||
sudo apt-get update -y
|
||||
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
|
||||
run: |
|
||||
flutter build linux --release --target lib/main_prod.dart
|
||||
|
@ -119,10 +115,6 @@ jobs:
|
|||
with:
|
||||
channel: "stable"
|
||||
|
||||
- name: Enable windows support
|
||||
run: |
|
||||
flutter config --enable-windows-desktop
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
flutter build windows --release --target lib/main_prod.dart
|
||||
|
@ -165,7 +157,6 @@ jobs:
|
|||
linux-build/*
|
||||
windows-build/*
|
||||
draft: true
|
||||
prerelease: true
|
||||
body_path: current-changelog.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
name: weblate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# every friday at 19:00 UTC
|
||||
- cron: "0 19 * * 5"
|
||||
|
@ -11,13 +12,19 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch changes
|
||||
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 merge weblate/master
|
||||
|
||||
- name: Regenerate l10n_from_string
|
||||
run: |
|
||||
dart run scripts/gen_l10n_from_string.dart
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3.12.0
|
||||
with:
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"L10n string": {
|
||||
"scope": "dart",
|
||||
"prefix": "l10n",
|
||||
"body": ["L10n.of(context)!.$0"]
|
||||
"body": ["L10n.of(context).$0"]
|
||||
},
|
||||
"Mobx store": {
|
||||
"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
|
||||
|
||||
### 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">
|
||||
|
||||
[![](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"/>
|
||||
|
||||
|
@ -43,22 +50,14 @@ The apk will be in `build/app/outputs/flutter-apk/app-prod-release.apk`
|
|||
### Linux
|
||||
|
||||
1. Make sure you have the additional [linux requirements](https://flutter.dev/desktop#additional-linux-requirements) (verify with `flutter doctor`)
|
||||
2. Enable linux desktop:
|
||||
```sh
|
||||
flutter config --enable-linux-desktop
|
||||
```
|
||||
3. Build: `flutter build linux --target lib/main_prod.dart --release`
|
||||
2. 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)
|
||||
|
||||
### Windows
|
||||
|
||||
1. Make sure you have the additional [windows requirements](https://flutter.dev/desktop#additional-windows-requirements) (verify with `flutter doctor`)
|
||||
2. Enable windows desktop:
|
||||
```sh
|
||||
flutter config --enable-windows-desktop
|
||||
```
|
||||
3. Build: `flutter build windows --target lib/main_prod.dart --release`
|
||||
2. 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)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ linter:
|
|||
- avoid_catching_errors
|
||||
- avoid_equals_and_hash_code_on_mutable_classes
|
||||
- avoid_escaping_inner_quotes
|
||||
- avoid_final_parameters
|
||||
- avoid_function_literals_in_foreach_calls
|
||||
- avoid_init_to_null
|
||||
- avoid_null_checks_in_equality_operators
|
||||
|
@ -29,6 +30,7 @@ linter:
|
|||
- cascade_invocations
|
||||
- cast_nullable_to_non_nullable
|
||||
- constant_identifier_names
|
||||
- conditional_uri_does_not_exist
|
||||
- curly_braces_in_flow_control_structures
|
||||
- directives_ordering
|
||||
- empty_catches
|
||||
|
@ -41,8 +43,11 @@ linter:
|
|||
- invariant_booleans
|
||||
- library_names
|
||||
- library_prefixes
|
||||
- literal_only_boolean_expressions
|
||||
- non_constant_identifier_names
|
||||
- noop_primitive_operations
|
||||
- no_leading_underscores_for_library_prefixes
|
||||
- no_leading_underscores_for_local_identifiers
|
||||
- null_check_on_nullable_type_parameter
|
||||
- omit_local_variable_types
|
||||
- one_member_abstracts
|
||||
|
@ -81,7 +86,9 @@ linter:
|
|||
- prefer_spread_collections
|
||||
- prefer_typing_uninitialized_variables
|
||||
- recursive_getters
|
||||
- secure_pubspec_urls
|
||||
- sized_box_for_whitespace
|
||||
- sized_box_shrink_expand
|
||||
- slash_for_doc_comments
|
||||
- sort_child_properties_last
|
||||
- sort_unnamed_constructors_first
|
||||
|
@ -94,6 +101,7 @@ linter:
|
|||
- unnecessary_constructor_name
|
||||
- unnecessary_getters_setters
|
||||
- unnecessary_lambdas
|
||||
- unnecessary_late
|
||||
- unnecessary_new
|
||||
- unnecessary_null_aware_assignments
|
||||
- unnecessary_null_checks
|
||||
|
@ -104,12 +112,15 @@ linter:
|
|||
- unnecessary_string_interpolations
|
||||
- unnecessary_this
|
||||
- unrelated_type_equality_checks
|
||||
- use_colored_box
|
||||
- use_enums
|
||||
- use_full_hex_values_for_flutter_colors
|
||||
- use_is_even_rather_than_modulo
|
||||
- use_named_constants
|
||||
- use_raw_strings
|
||||
- use_rethrow_when_possible
|
||||
- use_setters_to_change_properties
|
||||
- use_super_parameters
|
||||
- use_test_throws_matchers
|
||||
- use_to_and_as_if_applicable
|
||||
- void_checks
|
||||
|
|
|
@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
|
|||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion Math.max(flutter.compileSdkVersion, 32)
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
|
@ -47,8 +47,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "com.krawieck.lemmur"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
minSdkVersion Math.max(flutter.minSdkVersion, 16)
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:name="${applicationName}"
|
||||
android:label="${appName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.7.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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": {
|
||||
"placeholders": {
|
||||
"formattedCount": {}
|
||||
"formattedCount": {
|
||||
"type": "int",
|
||||
"format": "compact"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"placeholders": {
|
||||
"formattedCount": {}
|
||||
"formattedCount": {
|
||||
"type": "int",
|
||||
"format": "compact"
|
||||
}
|
||||
}
|
||||
},
|
||||
"number_of_users": "{formattedCount,plural, =1{{formattedCount} user} other{{formattedCount} users}}",
|
||||
"@number_of_users": {
|
||||
"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",
|
||||
|
@ -291,5 +309,135 @@
|
|||
"show_bot_accounts": "Show Bot Accounts",
|
||||
"@show_bot_accounts": {},
|
||||
"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",
|
||||
"settings": "設定",
|
||||
"@settings": {},
|
||||
"password": "パスワード",
|
||||
"@password": {},
|
||||
"email_or_username": "メールアドレスまたはユーザー名",
|
||||
"@email_or_username": {},
|
||||
"posts": "投稿",
|
||||
"@posts": {},
|
||||
"comments": "コメント",
|
||||
"@comments": {},
|
||||
"modlog": "モデレーションログ",
|
||||
"@modlog": {},
|
||||
"community": "コミュニティ",
|
||||
"@community": {},
|
||||
"url": "URL",
|
||||
"@url": {},
|
||||
"title": "タイトル",
|
||||
"@title": {},
|
||||
"body": "本文",
|
||||
"@body": {},
|
||||
"nsfw": "閲覧注意",
|
||||
"@nsfw": {},
|
||||
"post": "投稿",
|
||||
"@post": {},
|
||||
"save": "保存",
|
||||
"@save": {},
|
||||
"subscribed": "登録済み",
|
||||
"@subscribed": {},
|
||||
"local": "インスタンス内",
|
||||
"@local": {},
|
||||
"all": "全て",
|
||||
"@all": {},
|
||||
"replies": "返信",
|
||||
"@replies": {},
|
||||
"mentions": "言及",
|
||||
"@mentions": {},
|
||||
"from": "から",
|
||||
"@from": {},
|
||||
"to": "投稿先",
|
||||
"@to": {},
|
||||
"deleted_by_creator": "投稿者によって削除済み",
|
||||
"@deleted_by_creator": {},
|
||||
"more": "さらに表示",
|
||||
"@more": {},
|
||||
"mark_as_read": "既読にする",
|
||||
"@mark_as_read": {},
|
||||
"mark_as_unread": "未読にする",
|
||||
"@mark_as_unread": {},
|
||||
"reply": "返信",
|
||||
"@reply": {},
|
||||
"edit": "編集",
|
||||
"@edit": {},
|
||||
"delete": "削除",
|
||||
"@delete": {},
|
||||
"restore": "復元",
|
||||
"@restore": {},
|
||||
"yes": "はい",
|
||||
"@yes": {},
|
||||
"no": "いいえ",
|
||||
"@no": {},
|
||||
"avatar": "アバター",
|
||||
"@avatar": {},
|
||||
"banner": "バナー",
|
||||
"display_name": "アカウント名",
|
||||
"@banner": {},
|
||||
"display_name": "表示名",
|
||||
"@display_name": {},
|
||||
"bio": "自己紹介",
|
||||
"@bio": {},
|
||||
"email": "メールアドレス",
|
||||
"@email": {},
|
||||
"matrix_user": "Matrix のユーザーアカウント",
|
||||
"@matrix_user": {},
|
||||
"sort_type": "投稿やコメントの並び順",
|
||||
"type": "コミュニティの表示",
|
||||
"@sort_type": {},
|
||||
"type": "検索対象",
|
||||
"@type": {},
|
||||
"show_nsfw": "閲覧注意のコンテンツを表示",
|
||||
"@show_nsfw": {},
|
||||
"send_notifications_to_email": "通知を設定したメールアドレスに送信",
|
||||
"@send_notifications_to_email": {},
|
||||
"delete_account": "アカウントを削除",
|
||||
"@delete_account": {},
|
||||
"saved": "保存済み",
|
||||
"@saved": {},
|
||||
"communities": "コミュニティ",
|
||||
"@communities": {},
|
||||
"users": "ユーザー",
|
||||
"@users": {},
|
||||
"theme": "テーマ",
|
||||
"@theme": {},
|
||||
"language": "言語",
|
||||
"@language": {},
|
||||
"hot": "人気",
|
||||
"@hot": {},
|
||||
"new_": "新しい順",
|
||||
"@new_": {},
|
||||
"old": "古い順",
|
||||
"@old": {},
|
||||
"top": "トップ",
|
||||
"@top": {},
|
||||
"chat": "会話",
|
||||
"@chat": {},
|
||||
"admin": "管理者",
|
||||
"@admin": {},
|
||||
"by": "投稿者",
|
||||
"@by": {},
|
||||
"not_a_mod_or_admin": "モデレーターまたは管理者ではありません。",
|
||||
"@not_a_mod_or_admin": {},
|
||||
"not_an_admin": "管理者ではありません。",
|
||||
"@not_an_admin": {},
|
||||
"couldnt_find_post": "投稿が見つかりせんでした。",
|
||||
"@couldnt_find_post": {},
|
||||
"not_logged_in": "ログインしていません。",
|
||||
"@not_logged_in": {},
|
||||
"site_ban": "サイトへのアクセスを禁止されています",
|
||||
"@site_ban": {},
|
||||
"community_ban": "このコミュニティへのアクセスを禁止されています。",
|
||||
"@community_ban": {},
|
||||
"downvotes_disabled": "反対票を無効化",
|
||||
"@downvotes_disabled": {},
|
||||
"invalid_url": "無効なURL。",
|
||||
"@invalid_url": {},
|
||||
"locked": "凍結中",
|
||||
"@locked": {},
|
||||
"couldnt_create_comment": "投稿が作成されませんでした。",
|
||||
"@couldnt_create_comment": {},
|
||||
"couldnt_like_comment": "コメントを「いいね」できませんでした。",
|
||||
"@couldnt_like_comment": {},
|
||||
"couldnt_update_comment": "コメントが更新されませんでした。",
|
||||
"@couldnt_update_comment": {},
|
||||
"no_comment_edit_allowed": "コメントの編集権限がありません。",
|
||||
"@no_comment_edit_allowed": {},
|
||||
"couldnt_save_comment": "コメントが保存されませんでした。",
|
||||
"@couldnt_save_comment": {},
|
||||
"couldnt_get_comments": "コメントを読み込みできませんでした。",
|
||||
"@couldnt_get_comments": {},
|
||||
"invalid_post_title": "無効な投稿のタイトル",
|
||||
"@invalid_post_title": {},
|
||||
"couldnt_create_post": "投稿が作成されませんでした。",
|
||||
"@couldnt_create_post": {},
|
||||
"couldnt_like_post": "投稿を「いいね」できませんでした。",
|
||||
"@couldnt_like_post": {},
|
||||
"couldnt_find_community": "コミュニティが見つかりませんでした。",
|
||||
"@couldnt_find_community": {},
|
||||
"couldnt_get_posts": "投稿を読み込みできませんでした",
|
||||
"@couldnt_get_posts": {},
|
||||
"no_post_edit_allowed": "投稿の編集権限がありません。",
|
||||
"@no_post_edit_allowed": {},
|
||||
"couldnt_save_post": "投稿が保存されませんでした。",
|
||||
"@couldnt_save_post": {},
|
||||
"site_already_exists": "サイトは既に存在します。",
|
||||
"@site_already_exists": {},
|
||||
"couldnt_update_site": "サイトが更新されませんでした。",
|
||||
"invalid_community_name": "無効な名前です。",
|
||||
"@couldnt_update_site": {},
|
||||
"invalid_community_name": "無効なコミュニティの名前です。",
|
||||
"@invalid_community_name": {},
|
||||
"community_already_exists": "コミュニティは既に存在します。",
|
||||
"@community_already_exists": {},
|
||||
"community_moderator_already_exists": "コミュニティのモデレーターは既に存在しています。",
|
||||
"@community_moderator_already_exists": {},
|
||||
"community_follower_already_exists": "コミュニティへのフォロワーは既に存在します。",
|
||||
"@community_follower_already_exists": {},
|
||||
"not_a_moderator": "モデレーターではありません。",
|
||||
"@not_a_moderator": {},
|
||||
"couldnt_update_community": "コミュニティを更新できませんでした。",
|
||||
"@couldnt_update_community": {},
|
||||
"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": {},
|
||||
"couldnt_find_that_username_or_email": "ユーザー名またはメールアドレスが見つかりませんでした。",
|
||||
"@couldnt_find_that_username_or_email": {},
|
||||
"password_incorrect": "無効なパスワードです。",
|
||||
"@password_incorrect": {},
|
||||
"registration_closed": "新規登録は受け付けていません",
|
||||
"@registration_closed": {},
|
||||
"invalid_password": "無効なパスワードです。パスワードは必ず60文字以下にしてください。",
|
||||
"@invalid_password": {},
|
||||
"passwords_dont_match": "パスワードが一致しません。",
|
||||
"@passwords_dont_match": {},
|
||||
"captcha_incorrect": "Captchaコードが合っていません。",
|
||||
"invalid_username": "無効なユーザーネームです。",
|
||||
"@captcha_incorrect": {},
|
||||
"invalid_username": "無効なユーザー名です。",
|
||||
"@invalid_username": {},
|
||||
"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_post": "投稿が更新されませんでした",
|
||||
"@couldnt_update_post": {},
|
||||
"couldnt_create_private_message": "プライベートメッセージが作成されませんでした。",
|
||||
"@couldnt_create_private_message": {},
|
||||
"no_private_message_edit_allowed": "プライベートメッセージの編集許可がありません。",
|
||||
"@no_private_message_edit_allowed": {},
|
||||
"post_title_too_long": "投稿のタイトルが長すぎます。",
|
||||
"@post_title_too_long": {},
|
||||
"email_already_exists": "メールアドレスが既に使用されています。",
|
||||
"@email_already_exists": {},
|
||||
"user_already_exists": "ユーザーは既に存在します。",
|
||||
"number_of_users_online": "{formattedCount,plural, other{{formattedCount}名のユーザーがオンライン}}",
|
||||
"number_of_comments": "{formattedCount,plural, other{{formattedCount}件のコメント}}",
|
||||
"number_of_posts": "{formattedCount,plural, other{{formattedCount}件の投稿}}",
|
||||
"number_of_subscribers": "{formattedCount,plural, other{{formattedCount}名の登録者}}",
|
||||
"number_of_users": "{formattedCount,plural, other{{formattedCount}名のユーザー}}",
|
||||
"@user_already_exists": {},
|
||||
"number_of_users_online": "{formattedCount,plural, =1{{formattedCount} 名のユーザーがオンライン} other{{formattedCount} 名のユーザーがオンライン}}",
|
||||
"@number_of_users_online": {},
|
||||
"number_of_comments": "{formattedCount,plural, =1{{formattedCount} 件のコメント} 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": {},
|
||||
"subscribe": "登録",
|
||||
"@subscribe": {},
|
||||
"messages": "メッセージ",
|
||||
"@messages": {},
|
||||
"banned_users": "アクセス禁止してるユーザー",
|
||||
"delete_account_confirm": "警告: あなたの全てのデータをこのインスタンスから永久に削除します。連合機能によって他のインスタンスにあるあなたのデータは、削除されないかもしれません。確認のため、パスワードを入力してください。",
|
||||
"@banned_users": {},
|
||||
"delete_account_confirm": "警告: あなたの全てのデータは、このインスタンスから永久に削除されます。しかし、Lemmyの連合機能によって他のインスタンスにある、あなたの一部のデータは削除されないかもしれません。確認のため、パスワードを入力してください。",
|
||||
"@delete_account_confirm": {},
|
||||
"new_password": "新しいパスワード",
|
||||
"@new_password": {},
|
||||
"verify_password": "パスワードの確認",
|
||||
"@verify_password": {},
|
||||
"old_password": "現在のパスワード",
|
||||
"@old_password": {},
|
||||
"show_avatars": "アバターを表示",
|
||||
"@show_avatars": {},
|
||||
"search": "検索",
|
||||
"@search": {},
|
||||
"send_message": "メッセージを送信",
|
||||
"@send_message": {},
|
||||
"top_day": "今日の人気順",
|
||||
"@top_day": {},
|
||||
"top_week": "週間での人気順",
|
||||
"@top_week": {},
|
||||
"top_month": "月間での人気順",
|
||||
"@top_month": {},
|
||||
"top_year": "年間での人気順",
|
||||
"@top_year": {},
|
||||
"top_all": "全ての期間での人気順",
|
||||
"@top_all": {},
|
||||
"most_comments": "コメントの多い順",
|
||||
"@most_comments": {},
|
||||
"new_comments": "新しくコメントが入った順",
|
||||
"@new_comments": {},
|
||||
"active": "活発",
|
||||
"@active": {},
|
||||
"bot_account": "このアカウントをbotにする",
|
||||
"show_bot_accounts": "ボットアカウントを表示",
|
||||
"show_read_posts": "読んだ投稿を表示"
|
||||
"@bot_account": {},
|
||||
"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",
|
||||
"settings": "Ustawienia",
|
||||
"settings": "Ustawieniad",
|
||||
"@settings": {},
|
||||
"password": "Hasło",
|
||||
"@password": {},
|
||||
"email_or_username": "Email lub Login",
|
||||
"@email_or_username": {},
|
||||
"posts": "Posty",
|
||||
"@posts": {},
|
||||
"comments": "Komentarze",
|
||||
"@comments": {},
|
||||
"modlog": "Log moderatorski",
|
||||
"@modlog": {},
|
||||
"community": "Społeczność",
|
||||
"@community": {},
|
||||
"url": "Link",
|
||||
"@url": {},
|
||||
"title": "Tytuł",
|
||||
"@title": {},
|
||||
"body": "Treść",
|
||||
"@body": {},
|
||||
"nsfw": "NSFW (18+)",
|
||||
"@nsfw": {},
|
||||
"post": "post",
|
||||
"@post": {},
|
||||
"save": "zapisz",
|
||||
"@save": {},
|
||||
"subscribed": "Subskrybowane",
|
||||
"@subscribed": {},
|
||||
"local": "Lokalne",
|
||||
"@local": {},
|
||||
"all": "Wszystko",
|
||||
"@all": {},
|
||||
"replies": "Odpowiedzi",
|
||||
"@replies": {},
|
||||
"mentions": "Wzmianki",
|
||||
"@mentions": {},
|
||||
"from": "od",
|
||||
"@from": {},
|
||||
"to": "do",
|
||||
"@to": {},
|
||||
"deleted_by_creator": "usunięte przez osobę publikującą",
|
||||
"@deleted_by_creator": {},
|
||||
"more": "więcej",
|
||||
"@more": {},
|
||||
"mark_as_read": "zaznacz jako przeczytane",
|
||||
"@mark_as_read": {},
|
||||
"mark_as_unread": "zaznacz jako nieprzeczytane",
|
||||
"@mark_as_unread": {},
|
||||
"reply": "odpowiedz",
|
||||
"@reply": {},
|
||||
"edit": "edytuj",
|
||||
"@edit": {},
|
||||
"delete": "usuń",
|
||||
"@delete": {},
|
||||
"restore": "przywróć",
|
||||
"@restore": {},
|
||||
"yes": "tak",
|
||||
"@yes": {},
|
||||
"no": "nie",
|
||||
"@no": {},
|
||||
"avatar": "Awatar",
|
||||
"@avatar": {},
|
||||
"banner": "Baner",
|
||||
"@banner": {},
|
||||
"display_name": "Nazwa wyświetlana",
|
||||
"@display_name": {},
|
||||
"bio": "Opis",
|
||||
"@bio": {},
|
||||
"email": "Email",
|
||||
"@email": {},
|
||||
"matrix_user": "Login na Matrixie",
|
||||
"@matrix_user": {},
|
||||
"sort_type": "Sortuj typ",
|
||||
"@sort_type": {},
|
||||
"type": "Rodzaj",
|
||||
"@type": {},
|
||||
"show_nsfw": "Pokaż treści NSFW (18+)",
|
||||
"@show_nsfw": {},
|
||||
"send_notifications_to_email": "Wysyłaj powiadomienia na Email",
|
||||
"@send_notifications_to_email": {},
|
||||
"delete_account": "Usuń Konto",
|
||||
"@delete_account": {},
|
||||
"saved": "Zapisane",
|
||||
"@saved": {},
|
||||
"communities": "Społeczności",
|
||||
"@communities": {},
|
||||
"users": "Osoby zalogowane",
|
||||
"@users": {},
|
||||
"theme": "Motyw",
|
||||
"@theme": {},
|
||||
"language": "Język",
|
||||
"@language": {},
|
||||
"hot": "Popularne",
|
||||
"@hot": {},
|
||||
"new_": "Nowe",
|
||||
"@new_": {},
|
||||
"old": "Stare",
|
||||
"@old": {},
|
||||
"top": "Najpopularniejsze",
|
||||
"@top": {},
|
||||
"chat": "Dyskusja",
|
||||
"@chat": {},
|
||||
"admin": "admin",
|
||||
"@admin": {},
|
||||
"by": "przez",
|
||||
"@by": {},
|
||||
"not_a_mod_or_admin": "Nie moderuje ani administruje.",
|
||||
"@not_a_mod_or_admin": {},
|
||||
"not_an_admin": "Nie jest administratorem.",
|
||||
"@not_an_admin": {},
|
||||
"couldnt_find_post": "Nie udało się znaleźć posta.",
|
||||
"@couldnt_find_post": {},
|
||||
"not_logged_in": "Nie jesteś zalogowana/y.",
|
||||
"@not_logged_in": {},
|
||||
"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": {},
|
||||
"downvotes_disabled": "Wdółgłosy wyłączone",
|
||||
"@downvotes_disabled": {},
|
||||
"invalid_url": "Nieprawidłowy link.",
|
||||
"@invalid_url": {},
|
||||
"locked": "zablokowane",
|
||||
"@locked": {},
|
||||
"couldnt_create_comment": "Nie udało się stworzyć komentarza.",
|
||||
"@couldnt_create_comment": {},
|
||||
"couldnt_like_comment": "Polubienie komentarza nie powiodło się.",
|
||||
"@couldnt_like_comment": {},
|
||||
"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": {},
|
||||
"couldnt_save_comment": "Zapisanie komentarza nie powiodło się.",
|
||||
"@couldnt_save_comment": {},
|
||||
"couldnt_get_comments": "Pobranie komentarzy nie powiodło się.",
|
||||
"@couldnt_get_comments": {},
|
||||
"report_reason_required": "Wymagane jest uzasadnienie zgłoszenia.",
|
||||
"@report_reason_required": {},
|
||||
"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": {},
|
||||
"couldnt_resolve_report": "Nie udało się rozwiązać zgłoszenia.",
|
||||
"@couldnt_resolve_report": {},
|
||||
"invalid_post_title": "Nieprawidłowy tytuł posta",
|
||||
"@invalid_post_title": {},
|
||||
"couldnt_create_post": "Nie udało się stworzyć posta.",
|
||||
"@couldnt_create_post": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"couldnt_save_post": "Nie udało się zapisać posta.",
|
||||
"@couldnt_save_post": {},
|
||||
"site_already_exists": "Witryna już istnieje.",
|
||||
"@site_already_exists": {},
|
||||
"couldnt_update_site": "Nie udało się zaktualizować witryny.",
|
||||
"@couldnt_update_site": {},
|
||||
"invalid_community_name": "Niepoprawna nazwa.",
|
||||
"@invalid_community_name": {},
|
||||
"community_already_exists": "Społeczność już istnieje.",
|
||||
"@community_already_exists": {},
|
||||
"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": {},
|
||||
"not_a_moderator": "Nie jest moderatorem.",
|
||||
"@not_a_moderator": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"registration_closed": "Rejestracja Zamknięta",
|
||||
"@registration_closed": {},
|
||||
"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": {},
|
||||
"captcha_incorrect": "Captcha niepoprawna.",
|
||||
"@captcha_incorrect": {},
|
||||
"invalid_username": "Nieprawidłowy login.",
|
||||
"@invalid_username": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"post_title_too_long": "Tytuł posta zbyt długi.",
|
||||
"@post_title_too_long": {},
|
||||
"email_already_exists": "Email już istnieje.",
|
||||
"@email_already_exists": {},
|
||||
"user_already_exists": "Login już istnieje.",
|
||||
"@user_already_exists": {},
|
||||
"unsubscribe": "Odsubskrybuj",
|
||||
"@unsubscribe": {},
|
||||
"subscribe": "Subskrybuj",
|
||||
"@subscribe": {},
|
||||
"messages": "Wiadomości",
|
||||
"@messages": {},
|
||||
"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": {},
|
||||
"new_password": "Nowe Hasło",
|
||||
"@new_password": {},
|
||||
"verify_password": "Zweryfikuj Hasło",
|
||||
"@verify_password": {},
|
||||
"old_password": "Stare Hasło",
|
||||
"@old_password": {},
|
||||
"show_avatars": "Pokaż Awatary",
|
||||
"@show_avatars": {},
|
||||
"search": "Szukaj",
|
||||
"@search": {},
|
||||
"send_message": "Wyślij Wiadomość",
|
||||
"@send_message": {},
|
||||
"top_day": "Najpopularniejsze dziś",
|
||||
"@top_day": {},
|
||||
"top_week": "Najpopularniejsze tydzień",
|
||||
"@top_week": {},
|
||||
"top_month": "Najpopularniejsze miesiąc",
|
||||
"@top_month": {},
|
||||
"top_year": "Najpopularniejsze rok",
|
||||
"@top_year": {},
|
||||
"top_all": "Najpopularniejsze kiedykolwiek",
|
||||
"@top_all": {},
|
||||
"most_comments": "Najwięcej komentarzy",
|
||||
"@most_comments": {},
|
||||
"new_comments": "Nowe komentarze",
|
||||
"@new_comments": {},
|
||||
"active": "Aktywne",
|
||||
"@active": {},
|
||||
"bot_account": "Konto bota",
|
||||
"@bot_account": {},
|
||||
"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",
|
||||
"settings": "Configurações",
|
||||
"@settings": {},
|
||||
"password": "Senha",
|
||||
"@password": {},
|
||||
"email_or_username": "E-mail ou nome de usuário",
|
||||
"@email_or_username": {},
|
||||
"posts": "Publicações",
|
||||
"@posts": {},
|
||||
"comments": "Comentários",
|
||||
"@comments": {},
|
||||
"modlog": "Registro de moderação",
|
||||
"@modlog": {},
|
||||
"community": "Comunidade",
|
||||
"@community": {},
|
||||
"url": "URL",
|
||||
"@url": {},
|
||||
"title": "Título",
|
||||
"@title": {},
|
||||
"body": "Conteúdo",
|
||||
"@body": {},
|
||||
"nsfw": "NSFW",
|
||||
"@nsfw": {},
|
||||
"post": "publicação",
|
||||
"@post": {},
|
||||
"save": "guardar",
|
||||
"@save": {},
|
||||
"subscribed": "Inscrito",
|
||||
"@subscribed": {},
|
||||
"local": "Local",
|
||||
"@local": {},
|
||||
"all": "Tudo",
|
||||
"@all": {},
|
||||
"replies": "Respostas",
|
||||
"@replies": {},
|
||||
"mentions": "Menções",
|
||||
"@mentions": {},
|
||||
"from": "de",
|
||||
"@from": {},
|
||||
"to": "para",
|
||||
"@to": {},
|
||||
"deleted_by_creator": "apagado pelo criador",
|
||||
"@deleted_by_creator": {},
|
||||
"more": "mais",
|
||||
"@more": {},
|
||||
"mark_as_read": "marcar como lido",
|
||||
"@mark_as_read": {},
|
||||
"mark_as_unread": "marcar como não lido",
|
||||
"@mark_as_unread": {},
|
||||
"reply": "responder",
|
||||
"@reply": {},
|
||||
"edit": "editar",
|
||||
"@edit": {},
|
||||
"delete": "apagar",
|
||||
"@delete": {},
|
||||
"restore": "restaurar",
|
||||
"@restore": {},
|
||||
"yes": "sim",
|
||||
"@yes": {},
|
||||
"no": "não",
|
||||
"@no": {},
|
||||
"avatar": "Avatar",
|
||||
"@avatar": {},
|
||||
"banner": "Banner",
|
||||
"@banner": {},
|
||||
"display_name": "Nome de exibição",
|
||||
"@display_name": {},
|
||||
"bio": "Biografia",
|
||||
"@bio": {},
|
||||
"email": "E-mail",
|
||||
"@email": {},
|
||||
"matrix_user": "Usuário Matrix",
|
||||
"@matrix_user": {},
|
||||
"sort_type": "Ordenação",
|
||||
"@sort_type": {},
|
||||
"type": "Tipo",
|
||||
"@type": {},
|
||||
"show_nsfw": "Mostrar conteúdo NSFW",
|
||||
"@show_nsfw": {},
|
||||
"send_notifications_to_email": "Enviar notificações para o e-mail",
|
||||
"@send_notifications_to_email": {},
|
||||
"delete_account": "Apagar conta",
|
||||
"@delete_account": {},
|
||||
"saved": "Guardado",
|
||||
"@saved": {},
|
||||
"communities": "Comunidades",
|
||||
"@communities": {},
|
||||
"users": "Usuários",
|
||||
"@users": {},
|
||||
"theme": "Tema",
|
||||
"@theme": {},
|
||||
"language": "Idioma",
|
||||
"@language": {},
|
||||
"hot": "Popular",
|
||||
"@hot": {},
|
||||
"new_": "Novo",
|
||||
"@new_": {},
|
||||
"old": "Velho",
|
||||
"@old": {},
|
||||
"top": "Top",
|
||||
"@top": {},
|
||||
"chat": "Chat",
|
||||
"@chat": {},
|
||||
"admin": "administrador",
|
||||
"@admin": {},
|
||||
"by": "por",
|
||||
"@by": {},
|
||||
"not_a_mod_or_admin": "Não é moderador ou administrador.",
|
||||
"@not_a_mod_or_admin": {},
|
||||
"not_an_admin": "Não é administrador.",
|
||||
"@not_an_admin": {},
|
||||
"couldnt_find_post": "Não foi possível encontrar a publicação.",
|
||||
"@couldnt_find_post": {},
|
||||
"not_logged_in": "Não autenticado.",
|
||||
"@not_logged_in": {},
|
||||
"site_ban": "Você foi banido do site",
|
||||
"@site_ban": {},
|
||||
"community_ban": "Você foi banido desta comunidade.",
|
||||
"@community_ban": {},
|
||||
"downvotes_disabled": "Votos negativos desativados",
|
||||
"@downvotes_disabled": {},
|
||||
"invalid_url": "URL inválida.",
|
||||
"@invalid_url": {},
|
||||
"locked": "trancado",
|
||||
"@locked": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"couldnt_update_site": "Não foi possível atualizar o site.",
|
||||
"@couldnt_update_site": {},
|
||||
"invalid_community_name": "Nome inválido.",
|
||||
"@invalid_community_name": {},
|
||||
"community_already_exists": "Esta comunidade já existe.",
|
||||
"@community_already_exists": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"password_incorrect": "Senha incorreta.",
|
||||
"@password_incorrect": {},
|
||||
"registration_closed": "Registros desativados",
|
||||
"@registration_closed": {},
|
||||
"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": {},
|
||||
"captcha_incorrect": "Captcha incorreto.",
|
||||
"@captcha_incorrect": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"email_already_exists": "Este e-mail já existe.",
|
||||
"@email_already_exists": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"unsubscribe": "Cancelar inscrição",
|
||||
"@unsubscribe": {},
|
||||
"subscribe": "Inscrever-se",
|
||||
"@subscribe": {},
|
||||
"messages": "Mensagens",
|
||||
"@messages": {},
|
||||
"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": {},
|
||||
"new_password": "Nova senha",
|
||||
"@new_password": {},
|
||||
"verify_password": "Verifique a senha",
|
||||
"@verify_password": {},
|
||||
"old_password": "Senha antiga",
|
||||
"@old_password": {},
|
||||
"show_avatars": "Mostrar Avatares",
|
||||
"@show_avatars": {},
|
||||
"search": "Busca",
|
||||
"@search": {},
|
||||
"send_message": "Enviar mensagem",
|
||||
"@send_message": {},
|
||||
"top_day": "Melhor do dia",
|
||||
"@top_day": {},
|
||||
"top_week": "Melhor da semana",
|
||||
"@top_week": {},
|
||||
"top_month": "Melhor do mês",
|
||||
"@top_month": {},
|
||||
"top_year": "Melhor do ano",
|
||||
"@top_year": {},
|
||||
"most_comments": "Mais comentados",
|
||||
"@most_comments": {},
|
||||
"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"
|
||||
lane :prod do
|
||||
upload_to_play_store(
|
||||
track: "beta",
|
||||
package_name: "com.krawieck.lemmur",
|
||||
json_key: ENV["GOOGLE_SERVICE_ACCOUNT_KEY_PATH"],
|
||||
aab: ENV["ABB_PATH"]
|
||||
|
|
|
@ -1,37 +1,85 @@
|
|||
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_keyboard_visibility (0.0.1):
|
||||
- Flutter
|
||||
- image_picker (0.0.1):
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
- SDWebImage (5.12.5):
|
||||
- SDWebImage/Core (= 5.12.5)
|
||||
- SDWebImage/Core (5.12.5)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.3)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- 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`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_keyboard_visibility:
|
||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||
image_picker:
|
||||
:path: ".symlinks/plugins/image_picker/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_ios:
|
||||
|
@ -44,15 +92,20 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DKImagePickerController: 72fd378f244cef3d27288e0aebf217a4467e4012
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95
|
||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6
|
||||
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32
|
||||
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
|
||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
@ -42,7 +42,9 @@
|
|||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
|
||||
<!-- Image picker -->
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
|
@ -51,5 +53,7 @@
|
|||
<string>For uploading images for posts/avatars</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>For recording videos for posts</string>
|
||||
</dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -4,6 +4,5 @@ template-arb-file: intl_en.arb
|
|||
output-localization-file: l10n.dart
|
||||
preferred-supported-locales: [en]
|
||||
output-class: L10n
|
||||
untranslated-messages-file: assets/l10n/untranslated.json
|
||||
synthetic-package: false
|
||||
nullable-getter: false
|
||||
|
|
|
@ -5,13 +5,10 @@ import 'util/hot_rank.dart';
|
|||
enum CommentSortType {
|
||||
hot,
|
||||
top,
|
||||
// ignore: constant_identifier_names
|
||||
new_,
|
||||
old,
|
||||
chat,
|
||||
}
|
||||
chat;
|
||||
|
||||
extension on CommentSortType {
|
||||
/// returns a compare function for sorting a CommentTree according
|
||||
/// to the comment sort type
|
||||
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_subscribers = 'number_of_subscribers';
|
||||
static const number_of_users = 'number_of_users';
|
||||
static const number_of_communities = 'number_of_communities';
|
||||
static const unsubscribe = 'unsubscribe';
|
||||
static const subscribe = 'subscribe';
|
||||
static const messages = 'messages';
|
||||
|
@ -145,6 +146,24 @@ abstract class L10nStrings {
|
|||
static const bot_account = 'bot_account';
|
||||
static const show_bot_accounts = 'show_bot_accounts';
|
||||
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 {
|
||||
|
@ -406,6 +425,40 @@ extension L10nFromString on String {
|
|||
return L10n.of(context).show_bot_accounts;
|
||||
case L10nStrings.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:
|
||||
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
|
||||
class CommunitiesListPage extends StatelessWidget {
|
||||
final String title;
|
||||
final Future<List<CommunityView>> Function(
|
||||
int page,
|
||||
int batchSize,
|
||||
SortType sortType,
|
||||
) fetcher;
|
||||
final FetcherWithSorting<CommunityView> fetcher;
|
||||
|
||||
const CommunitiesListPage({Key? key, required this.fetcher, this.title = ''})
|
||||
: super(key: key);
|
||||
const CommunitiesListPage({
|
||||
super.key,
|
||||
required this.fetcher,
|
||||
this.title = '',
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.cardColor,
|
||||
|
@ -45,8 +45,7 @@ class CommunitiesListPage extends StatelessWidget {
|
|||
class CommunitiesListItem extends StatelessWidget {
|
||||
final CommunityView community;
|
||||
|
||||
const CommunitiesListItem({Key? key, required this.community})
|
||||
: super(key: key);
|
||||
const CommunitiesListItem({super.key, required this.community});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListTile(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fuzzy/fuzzy.dart';
|
||||
|
@ -15,6 +16,7 @@ import '../util/goto.dart';
|
|||
import '../util/text_color.dart';
|
||||
import '../widgets/avatar.dart';
|
||||
import '../widgets/pull_to_refresh.dart';
|
||||
import 'instance/instance.dart';
|
||||
|
||||
/// List of subscribed communities per instance
|
||||
class CommunitiesTab extends HookWidget {
|
||||
|
@ -59,15 +61,14 @@ class CommunitiesTab extends HookWidget {
|
|||
return Future.wait(futures);
|
||||
}
|
||||
|
||||
final _loggedInAccounts = accountsStore.loggedInInstances
|
||||
final loggedInAccounts = accountsStore.loggedInInstances
|
||||
.map((instanceHost) =>
|
||||
'$instanceHost${accountsStore.defaultUsernameFor(instanceHost)}')
|
||||
.toList();
|
||||
|
||||
final instancesRefreshable =
|
||||
useRefreshable(getInstances, _loggedInAccounts);
|
||||
final instancesRefreshable = useRefreshable(getInstances, loggedInAccounts);
|
||||
final communitiesRefreshable =
|
||||
useRefreshable(getCommunities, _loggedInAccounts);
|
||||
useRefreshable(getCommunities, loggedInAccounts);
|
||||
|
||||
if (communitiesRefreshable.snapshot.hasError ||
|
||||
instancesRefreshable.snapshot.hasError) {
|
||||
|
@ -112,8 +113,11 @@ class CommunitiesTab extends HookWidget {
|
|||
|
||||
final instances = instancesRefreshable.snapshot.data!;
|
||||
final communities = communitiesRefreshable.snapshot.data!
|
||||
..forEach((e) =>
|
||||
e.sort((a, b) => a.community.name.compareTo(b.community.name)));
|
||||
.map(
|
||||
(e) =>
|
||||
e.sorted((a, b) => a.community.name.compareTo(b.community.name)),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final filterIcon = () {
|
||||
if (filterController.text.isEmpty) {
|
||||
|
@ -171,8 +175,11 @@ class CommunitiesTab extends HookWidget {
|
|||
Column(
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () => goToInstance(context,
|
||||
accountsStore.loggedInInstances.elementAt(i)),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
InstancePage.route(
|
||||
accountsStore.loggedInInstances.elementAt(i),
|
||||
),
|
||||
),
|
||||
onLongPress: () => toggleCollapse(i),
|
||||
leading: Avatar(
|
||||
url: instances[i].icon,
|
||||
|
@ -234,9 +241,11 @@ class _CommunitySubscribeToggle extends HookWidget {
|
|||
final int communityId;
|
||||
final String instanceHost;
|
||||
|
||||
const _CommunitySubscribeToggle(
|
||||
{required this.instanceHost, required this.communityId, Key? key})
|
||||
: super(key: key);
|
||||
const _CommunitySubscribeToggle({
|
||||
required this.instanceHost,
|
||||
required this.communityId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -16,7 +16,7 @@ import '../../util/share.dart';
|
|||
import '../../widgets/failed_to_load.dart';
|
||||
import '../../widgets/reveal_after_scroll.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_more_menu.dart';
|
||||
import 'community_overview.dart';
|
||||
|
|
|
@ -15,8 +15,7 @@ import 'community_store.dart';
|
|||
class CommmunityAboutTab extends StatelessWidget {
|
||||
final FullCommunityView fullCommunityView;
|
||||
|
||||
const CommmunityAboutTab(this.fullCommunityView, {Key? key})
|
||||
: super(key: key);
|
||||
const CommmunityAboutTab(this.fullCommunityView, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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/logged_in_action.dart';
|
||||
import '../../url_launcher.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/mobx_provider.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
|
@ -14,8 +14,7 @@ import 'community_store.dart';
|
|||
class CommunityMoreMenu extends HookWidget {
|
||||
final FullCommunityView fullCommunityView;
|
||||
|
||||
const CommunityMoreMenu({Key? key, required this.fullCommunityView})
|
||||
: super(key: key);
|
||||
const CommunityMoreMenu({super.key, required this.fullCommunityView});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -28,10 +27,10 @@ class CommunityMoreMenu extends HookWidget {
|
|||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul.canLaunch(communityView.community.actorId)
|
||||
? ul.launch(communityView.community.actorId)
|
||||
: ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
onTap: () => launchLink(
|
||||
link: communityView.community.actorId,
|
||||
context: context,
|
||||
),
|
||||
),
|
||||
ObserverBuilder<CommunityStore>(builder: (context, store) {
|
||||
return ListTile(
|
||||
|
|
|
@ -4,10 +4,10 @@ import 'package:lemmy_api_client/v3.dart';
|
|||
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/goto.dart';
|
||||
import '../../widgets/avatar.dart';
|
||||
import '../../widgets/cached_network_image.dart';
|
||||
import '../../widgets/fullscreenable_image.dart';
|
||||
import '../instance/instance.dart';
|
||||
import 'community_follow_button.dart';
|
||||
|
||||
class CommunityOverview extends StatelessWidget {
|
||||
|
@ -93,9 +93,10 @@ class CommunityOverview extends StatelessWidget {
|
|||
text: community.community.originInstanceHost,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => goToInstance(
|
||||
context,
|
||||
community.community.originInstanceHost,
|
||||
..onTap = () => Navigator.of(context).push(
|
||||
InstancePage.route(
|
||||
community.community.originInstanceHost,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -6,17 +6,19 @@ part of 'community_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
|
||||
// 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 {
|
||||
final _$refreshAsyncAction = AsyncAction('_CommunityStore.refresh');
|
||||
late final _$refreshAsyncAction =
|
||||
AsyncAction('_CommunityStore.refresh', context: context);
|
||||
|
||||
@override
|
||||
Future<void> refresh(Jwt? token) {
|
||||
return _$refreshAsyncAction.run(() => super.refresh(token));
|
||||
}
|
||||
|
||||
final _$subscribeAsyncAction = AsyncAction('_CommunityStore.subscribe');
|
||||
late final _$subscribeAsyncAction =
|
||||
AsyncAction('_CommunityStore.subscribe', context: context);
|
||||
|
||||
@override
|
||||
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),
|
||||
};
|
||||
|
||||
const CommentSection({Key? key}) : super(key: key);
|
||||
const CommentSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -14,7 +14,6 @@ abstract class _FullPostStore with Store {
|
|||
final String instanceHost;
|
||||
|
||||
_FullPostStore({
|
||||
this.postStore,
|
||||
required this.postId,
|
||||
required this.instanceHost,
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'full_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
|
||||
// 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 {
|
||||
Computed<List<CommentTree>?>? _$commentTreeComputed;
|
||||
|
@ -38,7 +38,8 @@ mixin _$FullPostStore on _FullPostStore, Store {
|
|||
name: '_FullPostStore.comments'))
|
||||
.value;
|
||||
|
||||
final _$fullPostViewAtom = Atom(name: '_FullPostStore.fullPostView');
|
||||
late final _$fullPostViewAtom =
|
||||
Atom(name: '_FullPostStore.fullPostView', context: context);
|
||||
|
||||
@override
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
Future<void> refresh([Jwt? token]) {
|
||||
return _$refreshAsyncAction.run(() => super.refresh(token));
|
||||
}
|
||||
|
||||
final _$blockCommunityAsyncAction =
|
||||
AsyncAction('_FullPostStore.blockCommunity');
|
||||
late final _$blockCommunityAsyncAction =
|
||||
AsyncAction('_FullPostStore.blockCommunity', context: context);
|
||||
|
||||
@override
|
||||
Future<void> blockCommunity(Jwt token) {
|
||||
return _$blockCommunityAsyncAction.run(() => super.blockCommunity(token));
|
||||
}
|
||||
|
||||
final _$_FullPostStoreActionController =
|
||||
ActionController(name: '_FullPostStore');
|
||||
late final _$_FullPostStoreActionController =
|
||||
ActionController(name: '_FullPostStore', context: context);
|
||||
|
||||
@override
|
||||
void updateSorting(CommentSortType sort) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||
|
||||
import '../util/extensions/brightness.dart';
|
||||
import 'communities_tab.dart';
|
||||
import 'create_post.dart';
|
||||
import 'create_post/create_post_fab.dart';
|
||||
import 'home_tab.dart';
|
||||
import 'profile_tab.dart';
|
||||
import 'search_tab.dart';
|
||||
|
|
|
@ -15,6 +15,7 @@ import '../widgets/cached_network_image.dart';
|
|||
import '../widgets/infinite_scroll.dart';
|
||||
import '../widgets/sortable_infinite_list.dart';
|
||||
import 'inbox.dart';
|
||||
import 'instance/instance.dart';
|
||||
import 'settings/add_account_page.dart';
|
||||
|
||||
/// First thing users sees when opening the app
|
||||
|
@ -128,7 +129,9 @@ class HomeTab extends HookWidget {
|
|||
color:
|
||||
theme.textTheme.bodyText1?.color?.withOpacity(0.7)),
|
||||
),
|
||||
onTap: () => goToInstance(context, instance),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
InstancePage.route(instance),
|
||||
),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
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
|
||||
// **************************************************************************
|
||||
|
||||
// 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 {
|
||||
final _$_LogConsolePageStoreActionController =
|
||||
ActionController(name: '_LogConsolePageStore');
|
||||
late final _$_LogConsolePageStoreActionController =
|
||||
ActionController(name: '_LogConsolePageStore', context: context);
|
||||
|
||||
@override
|
||||
void addLog(LogRecord logRecord) {
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
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 'package:url_launcher/url_launcher.dart' as ul;
|
||||
|
||||
import '../hooks/delayed_loading.dart';
|
||||
import '../hooks/image_picker.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../url_launcher.dart';
|
||||
import '../util/files.dart';
|
||||
import '../util/icons.dart';
|
||||
import '../util/pictrs.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/bottom_safe.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
|
||||
/// This page will assume the manage account is logged in and
|
||||
|
@ -48,13 +47,12 @@ class ManageAccountPage extends HookWidget {
|
|||
final userProfileUrl =
|
||||
await userFuture.then((e) => e.person.actorId);
|
||||
|
||||
if (await ul.canLaunch(userProfileUrl)) {
|
||||
await ul.launch(userProfileUrl);
|
||||
final didLaunch = await launchLink(
|
||||
link: userProfileUrl,
|
||||
context: context,
|
||||
);
|
||||
if (didLaunch) {
|
||||
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 {
|
||||
const _ManageAccount({Key? key, required this.user}) : super(key: key);
|
||||
const _ManageAccount({required this.user});
|
||||
|
||||
final LocalUserSettingsView user;
|
||||
|
||||
|
@ -101,7 +99,6 @@ class _ManageAccount extends HookWidget {
|
|||
|
||||
final displayNameController =
|
||||
useTextEditingController(text: user.person.displayName);
|
||||
final bioController = useTextEditingController(text: user.person.bio);
|
||||
final emailController =
|
||||
useTextEditingController(text: user.localUser.email);
|
||||
final matrixUserController =
|
||||
|
@ -124,13 +121,15 @@ class _ManageAccount extends HookWidget {
|
|||
|
||||
final deleteAccountPasswordController = useTextEditingController();
|
||||
|
||||
final bioFocusNode = useFocusNode();
|
||||
final emailFocusNode = useFocusNode();
|
||||
final matrixUserFocusNode = useFocusNode();
|
||||
final newPasswordFocusNode = useFocusNode();
|
||||
// final verifyPasswordFocusNode = useFocusNode();
|
||||
// final oldPasswordFocusNode = useFocusNode();
|
||||
|
||||
final bioController = useEditorController(
|
||||
instanceHost: user.instanceHost, text: user.person.bio);
|
||||
|
||||
final token =
|
||||
accountsStore.userDataFor(user.instanceHost, user.person.name)!.jwt;
|
||||
|
||||
|
@ -158,7 +157,9 @@ class _ManageAccount extends HookWidget {
|
|||
displayName: displayNameController.text.isEmpty
|
||||
? null
|
||||
: 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,
|
||||
));
|
||||
|
||||
|
@ -236,150 +237,157 @@ class _ManageAccount extends HookWidget {
|
|||
}
|
||||
}
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
return Stack(
|
||||
children: [
|
||||
_ImagePicker(
|
||||
user: user,
|
||||
name: L10n.of(context).avatar,
|
||||
initialUrl: avatar.value,
|
||||
onChange: (value) => avatar.value = value,
|
||||
informAcceptedRef: informAcceptedAvatarRef,
|
||||
ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
children: [
|
||||
_ImagePicker(
|
||||
user: user,
|
||||
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),
|
||||
_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: (_) => 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(),
|
||||
BottomSticky(
|
||||
child: EditorToolbar(bioController),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -398,13 +406,12 @@ class _ImagePicker extends HookWidget {
|
|||
final ObjectRef<VoidCallback?> informAcceptedRef;
|
||||
|
||||
const _ImagePicker({
|
||||
Key? key,
|
||||
required this.initialUrl,
|
||||
required this.name,
|
||||
required this.user,
|
||||
required this.onChange,
|
||||
required this.informAcceptedRef,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -415,13 +422,12 @@ class _ImagePicker extends HookWidget {
|
|||
final url = useState(initialUrl.value);
|
||||
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
|
||||
|
||||
final imagePicker = useImagePicker();
|
||||
final accountsStore = useAccountsStore();
|
||||
final delayedLoading = useDelayedLoading();
|
||||
|
||||
uploadImage() async {
|
||||
try {
|
||||
final pic = await imagePicker.pickImage(source: ImageSource.gallery);
|
||||
final pic = await pickImage();
|
||||
// pic is null when the picker was cancelled
|
||||
if (pic != null) {
|
||||
delayedLoading.start();
|
||||
|
|
|
@ -12,6 +12,7 @@ abstract class _ModlogPageStore with Store, DisposableStore {
|
|||
final String instanceHost;
|
||||
final int? communityId;
|
||||
|
||||
// ignore: unused_element
|
||||
_ModlogPageStore(this.instanceHost, [this.communityId]) {
|
||||
addReaction(reaction((_) => page, (_) => fetchPage()));
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'modlog_page_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
|
||||
// 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 {
|
||||
Computed<bool>? _$hasPreviousPageComputed;
|
||||
|
@ -24,7 +24,7 @@ mixin _$ModlogPageStore on _ModlogPageStore, Store {
|
|||
name: '_ModlogPageStore.hasNextPage'))
|
||||
.value;
|
||||
|
||||
final _$pageAtom = Atom(name: '_ModlogPageStore.page');
|
||||
late final _$pageAtom = Atom(name: '_ModlogPageStore.page', context: context);
|
||||
|
||||
@override
|
||||
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
|
||||
Future<void> fetchPage() {
|
||||
return _$fetchPageAsyncAction.run(() => super.fetchPage());
|
||||
}
|
||||
|
||||
final _$_ModlogPageStoreActionController =
|
||||
ActionController(name: '_ModlogPageStore');
|
||||
late final _$_ModlogPageStoreActionController =
|
||||
ActionController(name: '_ModlogPageStore', context: context);
|
||||
|
||||
@override
|
||||
void previousPage() {
|
||||
|
|
|
@ -8,7 +8,7 @@ import '../../widgets/avatar.dart';
|
|||
import 'modlog_entry.dart';
|
||||
|
||||
class ModlogTable extends StatelessWidget {
|
||||
const ModlogTable({Key? key, required this.modlog}) : super(key: key);
|
||||
const ModlogTable({super.key, required this.modlog});
|
||||
|
||||
final Modlog modlog;
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v3.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||
|
||||
import '../../hooks/delayed_loading.dart';
|
||||
import '../../hooks/stores.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../stores/accounts_store.dart';
|
||||
import '../../stores/config_store.dart';
|
||||
import '../../url_launcher.dart';
|
||||
import '../../widgets/cached_network_image.dart';
|
||||
import '../../widgets/fullscreenable_image.dart';
|
||||
import '../../widgets/radio_picker.dart';
|
||||
|
@ -173,8 +173,11 @@ class AddAccountPage extends HookWidget {
|
|||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: extract to LemmyUrls or something
|
||||
ul.launch('https://${selectedInstance.value}/login');
|
||||
launchLink(
|
||||
// TODO: extract to LemmyUrls or something
|
||||
link: 'https://${selectedInstance.value}/login',
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Text('Register'),
|
||||
),
|
||||
|
|
|
@ -10,7 +10,7 @@ import 'community_block_store.dart';
|
|||
import 'user_block_store.dart';
|
||||
|
||||
class BlockPersonTile extends StatelessWidget {
|
||||
const BlockPersonTile({Key? key}) : super(key: key);
|
||||
const BlockPersonTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -40,7 +40,7 @@ class BlockPersonTile extends StatelessWidget {
|
|||
}
|
||||
|
||||
class BlockCommunityTile extends HookWidget {
|
||||
const BlockCommunityTile({Key? key}) : super(key: key);
|
||||
const BlockCommunityTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'blocks_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
|
||||
// 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 {
|
||||
Computed<Iterable<UserBlockStore>?>? _$blockedUsersComputed;
|
||||
|
@ -32,7 +32,8 @@ mixin _$BlocksStore on _BlocksStore, Store {
|
|||
Computed<bool>(() => super.isUsable, name: '_BlocksStore.isUsable'))
|
||||
.value;
|
||||
|
||||
final _$_blockedUsersAtom = Atom(name: '_BlocksStore._blockedUsers');
|
||||
late final _$_blockedUsersAtom =
|
||||
Atom(name: '_BlocksStore._blockedUsers', context: context);
|
||||
|
||||
@override
|
||||
ObservableList<UserBlockStore>? get _blockedUsers {
|
||||
|
@ -47,8 +48,8 @@ mixin _$BlocksStore on _BlocksStore, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$_blockedCommunitiesAtom =
|
||||
Atom(name: '_BlocksStore._blockedCommunities');
|
||||
late final _$_blockedCommunitiesAtom =
|
||||
Atom(name: '_BlocksStore._blockedCommunities', context: context);
|
||||
|
||||
@override
|
||||
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
|
||||
Future<void> blockUser(Jwt token, int id) {
|
||||
return _$blockUserAsyncAction.run(() => super.blockUser(token, id));
|
||||
}
|
||||
|
||||
final _$blockCommunityAsyncAction =
|
||||
AsyncAction('_BlocksStore.blockCommunity');
|
||||
late final _$blockCommunityAsyncAction =
|
||||
AsyncAction('_BlocksStore.blockCommunity', context: context);
|
||||
|
||||
@override
|
||||
Future<void> blockCommunity(Jwt token, int id) {
|
||||
|
@ -79,7 +81,8 @@ mixin _$BlocksStore on _BlocksStore, Store {
|
|||
.run(() => super.blockCommunity(token, id));
|
||||
}
|
||||
|
||||
final _$refreshAsyncAction = AsyncAction('_BlocksStore.refresh');
|
||||
late final _$refreshAsyncAction =
|
||||
AsyncAction('_BlocksStore.refresh', context: context);
|
||||
|
||||
@override
|
||||
Future<void> refresh() {
|
||||
|
|
|
@ -6,10 +6,11 @@ part of 'community_block_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
|
||||
// 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 {
|
||||
final _$blockedAtom = Atom(name: '_CommunityBlockStore.blocked');
|
||||
late final _$blockedAtom =
|
||||
Atom(name: '_CommunityBlockStore.blocked', context: context);
|
||||
|
||||
@override
|
||||
bool get blocked {
|
||||
|
|
|
@ -6,10 +6,11 @@ part of 'user_block_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
|
||||
// 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 {
|
||||
final _$blockedAtom = Atom(name: '_UserBlockStore.blocked');
|
||||
late final _$blockedAtom =
|
||||
Atom(name: '_UserBlockStore.blocked', context: context);
|
||||
|
||||
@override
|
||||
bool get blocked {
|
||||
|
|
|
@ -185,10 +185,9 @@ class _AccountOptions extends HookWidget {
|
|||
final String username;
|
||||
|
||||
const _AccountOptions({
|
||||
Key? key,
|
||||
required this.instanceHost,
|
||||
required this.username,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -4,29 +4,34 @@ import 'package:lemmy_api_client/v3.dart';
|
|||
import '../util/extensions/api.dart';
|
||||
import '../util/goto.dart';
|
||||
import '../widgets/avatar.dart';
|
||||
import '../widgets/infinite_scroll.dart';
|
||||
import '../widgets/markdown_text.dart';
|
||||
|
||||
/// Infinite list of Users fetched by the given fetcher
|
||||
class UsersListPage extends StatelessWidget {
|
||||
final String title;
|
||||
final List<PersonViewSafe> users;
|
||||
final Fetcher<PersonViewSafe> fetcher;
|
||||
|
||||
const UsersListPage({Key? key, required this.users, this.title = ''})
|
||||
: super(key: key);
|
||||
const UsersListPage({super.key, required this.fetcher, this.title = ''});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// TODO: change to infinite scroll
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.cardColor,
|
||||
title: Text(title),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemBuilder: (context, i) => UsersListItem(user: users[i]),
|
||||
itemCount: users.length,
|
||||
body: InfiniteScroll<PersonViewSafe>(
|
||||
fetcher: fetcher,
|
||||
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 {
|
||||
final PersonViewSafe user;
|
||||
|
||||
const UsersListItem({Key? key, required this.user}) : super(key: key);
|
||||
const UsersListItem({super.key, required this.user});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListTile(
|
||||
title: Text(user.person.originPreferredName),
|
||||
subtitle: user.person.bio != null
|
||||
? Opacity(
|
||||
opacity: 0.7,
|
||||
child: MarkdownText(
|
||||
user.person.bio!,
|
||||
instanceHost: user.instanceHost,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () => goToUser.fromPersonSafe(context, user.person),
|
||||
leading: Avatar(url: user.person.avatar),
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(user.person.originPreferredName),
|
||||
subtitle: user.person.bio != null
|
||||
? Opacity(
|
||||
opacity: 0.7,
|
||||
child: MarkdownText(
|
||||
user.person.bio!,
|
||||
instanceHost: user.instanceHost,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
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) =>
|
||||
<String, dynamic>{
|
||||
'theme': _$ThemeModeEnumMap[instance.theme],
|
||||
'theme': _$ThemeModeEnumMap[instance.theme]!,
|
||||
'amoledDarkMode': instance.amoledDarkMode,
|
||||
'locale': const LocaleConverter().toJson(instance.locale),
|
||||
'showAvatars': instance.showAvatars,
|
||||
|
@ -38,10 +38,10 @@ const _$ThemeModeEnumMap = {
|
|||
// 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 {
|
||||
final _$themeAtom = Atom(name: '_ConfigStore.theme');
|
||||
late final _$themeAtom = Atom(name: '_ConfigStore.theme', context: context);
|
||||
|
||||
@override
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
SortType get defaultSortType {
|
||||
|
@ -131,8 +135,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$defaultListingTypeAtom =
|
||||
Atom(name: '_ConfigStore.defaultListingType');
|
||||
late final _$defaultListingTypeAtom =
|
||||
Atom(name: '_ConfigStore.defaultListingType', context: context);
|
||||
|
||||
@override
|
||||
PostListingType get defaultListingType {
|
||||
|
@ -147,8 +151,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$importLemmyUserSettingsAsyncAction =
|
||||
AsyncAction('_ConfigStore.importLemmyUserSettings');
|
||||
late final _$importLemmyUserSettingsAsyncAction =
|
||||
AsyncAction('_ConfigStore.importLemmyUserSettings', context: context);
|
||||
|
||||
@override
|
||||
Future<void> importLemmyUserSettings(Jwt token) {
|
||||
|
@ -156,7 +160,8 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
|||
.run(() => super.importLemmyUserSettings(token));
|
||||
}
|
||||
|
||||
final _$_ConfigStoreActionController = ActionController(name: '_ConfigStore');
|
||||
late final _$_ConfigStoreActionController =
|
||||
ActionController(name: '_ConfigStore', context: context);
|
||||
|
||||
@override
|
||||
void copyLemmyUserSettings(LocalUserSettings localUserSettings) {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||
|
||||
import 'l10n/l10n.dart';
|
||||
import 'pages/community/community.dart';
|
||||
import 'pages/instance.dart';
|
||||
import 'pages/instance/instance.dart';
|
||||
import 'pages/media_view.dart';
|
||||
import 'pages/user.dart';
|
||||
import 'stores/accounts_store.dart';
|
||||
|
@ -23,7 +25,10 @@ Future<void> linkLauncher({
|
|||
final instances = context.read<AccountsStore>().instances;
|
||||
|
||||
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
|
||||
if (url.startsWith('/u/')) {
|
||||
|
@ -48,7 +53,7 @@ Future<void> linkLauncher({
|
|||
|
||||
if (matchedInstance != null && instances.any((e) => e == match?.group(1))) {
|
||||
if (rest == null || rest.isEmpty || rest == '/') {
|
||||
return push(() => InstancePage(instanceHost: matchedInstance));
|
||||
return Navigator.of(context).push<void>(InstancePage.route(instanceHost));
|
||||
}
|
||||
final split = rest.split('/');
|
||||
switch (split[1]) {
|
||||
|
@ -97,14 +102,25 @@ Future<void> linkLauncher({
|
|||
|
||||
// FALLBACK TO REGULAR LINK STUFF
|
||||
|
||||
return openInBrowser(url);
|
||||
await launchLink(link: url, context: context);
|
||||
}
|
||||
|
||||
Future<void> openInBrowser(String url) async {
|
||||
if (await ul.canLaunch(url)) {
|
||||
await ul.launch(url);
|
||||
final _logger = Logger('launchLink');
|
||||
|
||||
/// 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 {
|
||||
throw Exception();
|
||||
// TODO: handle opening links to stuff in app
|
||||
_logger.warning('Failed to launch a link: $link');
|
||||
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:mobx/mobx.dart';
|
||||
|
||||
import '../l10n/l10n_from_string.dart';
|
||||
|
||||
part 'async_store.freezed.dart';
|
||||
part 'async_store.g.dart';
|
||||
|
||||
|
@ -28,6 +30,10 @@ abstract class _AsyncStore<T> with Store {
|
|||
@action
|
||||
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].
|
||||
/// 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
|
||||
|
@ -51,11 +57,10 @@ abstract class _AsyncStore<T> with Store {
|
|||
|
||||
return result;
|
||||
} on SocketException {
|
||||
// TODO: use an existing l10n key
|
||||
if (data != null) {
|
||||
asyncState = data.copyWith(errorTerm: 'network_error');
|
||||
asyncState = data.copyWith(errorTerm: L10nStrings.network_error);
|
||||
} else {
|
||||
asyncState = const AsyncState.error('network_error');
|
||||
asyncState = const AsyncState.error(L10nStrings.network_error);
|
||||
}
|
||||
} catch (err) {
|
||||
if (data != null) {
|
||||
|
@ -65,6 +70,8 @@ abstract class _AsyncStore<T> with Store {
|
|||
}
|
||||
rethrow;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [run] but specialized for a [LemmyApiQuery].
|
||||
|
@ -76,8 +83,10 @@ abstract class _AsyncStore<T> with Store {
|
|||
bool refresh = false,
|
||||
}) async {
|
||||
try {
|
||||
return await run(() => LemmyApiV3(instanceHost).run(query),
|
||||
refresh: refresh);
|
||||
return await run(
|
||||
() => LemmyApiV3(instanceHost).run(query),
|
||||
refresh: refresh,
|
||||
);
|
||||
} on LemmyApiException catch (err) {
|
||||
final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null;
|
||||
if (data != null) {
|
||||
|
@ -86,6 +95,8 @@ abstract class _AsyncStore<T> with Store {
|
|||
asyncState = AsyncState<T>.error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// helper function for mapping [asyncState] into 3 variants
|
||||
|
|
|
@ -12,36 +12,7 @@ part of 'async_store.dart';
|
|||
T _$identity<T>(T value) => value;
|
||||
|
||||
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');
|
||||
|
||||
/// @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();
|
||||
'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
|
||||
mixin _$AsyncState<T> {
|
||||
|
@ -115,22 +86,22 @@ class _$AsyncStateCopyWithImpl<T, $Res>
|
|||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AsyncStateInitialCopyWith<T, $Res> {
|
||||
factory $AsyncStateInitialCopyWith(AsyncStateInitial<T> value,
|
||||
$Res Function(AsyncStateInitial<T>) then) =
|
||||
_$AsyncStateInitialCopyWithImpl<T, $Res>;
|
||||
abstract class _$$AsyncStateInitialCopyWith<T, $Res> {
|
||||
factory _$$AsyncStateInitialCopyWith(_$AsyncStateInitial<T> value,
|
||||
$Res Function(_$AsyncStateInitial<T>) then) =
|
||||
__$$AsyncStateInitialCopyWithImpl<T, $Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AsyncStateInitialCopyWithImpl<T, $Res>
|
||||
class __$$AsyncStateInitialCopyWithImpl<T, $Res>
|
||||
extends _$AsyncStateCopyWithImpl<T, $Res>
|
||||
implements $AsyncStateInitialCopyWith<T, $Res> {
|
||||
_$AsyncStateInitialCopyWithImpl(
|
||||
AsyncStateInitial<T> _value, $Res Function(AsyncStateInitial<T>) _then)
|
||||
: super(_value, (v) => _then(v as AsyncStateInitial<T>));
|
||||
implements _$$AsyncStateInitialCopyWith<T, $Res> {
|
||||
__$$AsyncStateInitialCopyWithImpl(_$AsyncStateInitial<T> _value,
|
||||
$Res Function(_$AsyncStateInitial<T>) _then)
|
||||
: super(_value, (v) => _then(v as _$AsyncStateInitial<T>));
|
||||
|
||||
@override
|
||||
AsyncStateInitial<T> get _value => super._value as AsyncStateInitial<T>;
|
||||
_$AsyncStateInitial<T> get _value => super._value as _$AsyncStateInitial<T>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
@ -148,13 +119,13 @@ class _$AsyncStateInitial<T>
|
|||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties..add(DiagnosticsProperty('type', 'AsyncState<$T>.initial'));
|
||||
properties.add(DiagnosticsProperty('type', 'AsyncState<$T>.initial'));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType && other is AsyncStateInitial<T>);
|
||||
(other.runtimeType == runtimeType && other is _$AsyncStateInitial<T>);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -240,30 +211,30 @@ abstract class AsyncStateInitial<T> implements AsyncState<T> {
|
|||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AsyncStateDataCopyWith<T, $Res> {
|
||||
factory $AsyncStateDataCopyWith(
|
||||
AsyncStateData<T> value, $Res Function(AsyncStateData<T>) then) =
|
||||
_$AsyncStateDataCopyWithImpl<T, $Res>;
|
||||
abstract class _$$AsyncStateDataCopyWith<T, $Res> {
|
||||
factory _$$AsyncStateDataCopyWith(
|
||||
_$AsyncStateData<T> value, $Res Function(_$AsyncStateData<T>) then) =
|
||||
__$$AsyncStateDataCopyWithImpl<T, $Res>;
|
||||
$Res call({T data, String? errorTerm});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AsyncStateDataCopyWithImpl<T, $Res>
|
||||
class __$$AsyncStateDataCopyWithImpl<T, $Res>
|
||||
extends _$AsyncStateCopyWithImpl<T, $Res>
|
||||
implements $AsyncStateDataCopyWith<T, $Res> {
|
||||
_$AsyncStateDataCopyWithImpl(
|
||||
AsyncStateData<T> _value, $Res Function(AsyncStateData<T>) _then)
|
||||
: super(_value, (v) => _then(v as AsyncStateData<T>));
|
||||
implements _$$AsyncStateDataCopyWith<T, $Res> {
|
||||
__$$AsyncStateDataCopyWithImpl(
|
||||
_$AsyncStateData<T> _value, $Res Function(_$AsyncStateData<T>) _then)
|
||||
: super(_value, (v) => _then(v as _$AsyncStateData<T>));
|
||||
|
||||
@override
|
||||
AsyncStateData<T> get _value => super._value as AsyncStateData<T>;
|
||||
_$AsyncStateData<T> get _value => super._value as _$AsyncStateData<T>;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? data = freezed,
|
||||
Object? errorTerm = freezed,
|
||||
}) {
|
||||
return _then(AsyncStateData<T>(
|
||||
return _then(_$AsyncStateData<T>(
|
||||
data == freezed
|
||||
? _value.data
|
||||
: data // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -306,7 +277,7 @@ class _$AsyncStateData<T>
|
|||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is AsyncStateData<T> &&
|
||||
other is _$AsyncStateData<T> &&
|
||||
const DeepCollectionEquality().equals(other.data, data) &&
|
||||
const DeepCollectionEquality().equals(other.errorTerm, errorTerm));
|
||||
}
|
||||
|
@ -319,8 +290,8 @@ class _$AsyncStateData<T>
|
|||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$AsyncStateDataCopyWith<T, AsyncStateData<T>> get copyWith =>
|
||||
_$AsyncStateDataCopyWithImpl<T, AsyncStateData<T>>(this, _$identity);
|
||||
_$$AsyncStateDataCopyWith<T, _$AsyncStateData<T>> get copyWith =>
|
||||
__$$AsyncStateDataCopyWithImpl<T, _$AsyncStateData<T>>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
|
@ -398,33 +369,33 @@ class _$AsyncStateData<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>;
|
||||
|
||||
T get data;
|
||||
String? get errorTerm;
|
||||
@JsonKey(ignore: true)
|
||||
$AsyncStateDataCopyWith<T, AsyncStateData<T>> get copyWith =>
|
||||
_$$AsyncStateDataCopyWith<T, _$AsyncStateData<T>> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AsyncStateLoadingCopyWith<T, $Res> {
|
||||
factory $AsyncStateLoadingCopyWith(AsyncStateLoading<T> value,
|
||||
$Res Function(AsyncStateLoading<T>) then) =
|
||||
_$AsyncStateLoadingCopyWithImpl<T, $Res>;
|
||||
abstract class _$$AsyncStateLoadingCopyWith<T, $Res> {
|
||||
factory _$$AsyncStateLoadingCopyWith(_$AsyncStateLoading<T> value,
|
||||
$Res Function(_$AsyncStateLoading<T>) then) =
|
||||
__$$AsyncStateLoadingCopyWithImpl<T, $Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AsyncStateLoadingCopyWithImpl<T, $Res>
|
||||
class __$$AsyncStateLoadingCopyWithImpl<T, $Res>
|
||||
extends _$AsyncStateCopyWithImpl<T, $Res>
|
||||
implements $AsyncStateLoadingCopyWith<T, $Res> {
|
||||
_$AsyncStateLoadingCopyWithImpl(
|
||||
AsyncStateLoading<T> _value, $Res Function(AsyncStateLoading<T>) _then)
|
||||
: super(_value, (v) => _then(v as AsyncStateLoading<T>));
|
||||
implements _$$AsyncStateLoadingCopyWith<T, $Res> {
|
||||
__$$AsyncStateLoadingCopyWithImpl(_$AsyncStateLoading<T> _value,
|
||||
$Res Function(_$AsyncStateLoading<T>) _then)
|
||||
: super(_value, (v) => _then(v as _$AsyncStateLoading<T>));
|
||||
|
||||
@override
|
||||
AsyncStateLoading<T> get _value => super._value as AsyncStateLoading<T>;
|
||||
_$AsyncStateLoading<T> get _value => super._value as _$AsyncStateLoading<T>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
@ -442,13 +413,13 @@ class _$AsyncStateLoading<T>
|
|||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties..add(DiagnosticsProperty('type', 'AsyncState<$T>.loading'));
|
||||
properties.add(DiagnosticsProperty('type', 'AsyncState<$T>.loading'));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType && other is AsyncStateLoading<T>);
|
||||
(other.runtimeType == runtimeType && other is _$AsyncStateLoading<T>);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -534,29 +505,29 @@ abstract class AsyncStateLoading<T> implements AsyncState<T> {
|
|||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AsyncStateErrorCopyWith<T, $Res> {
|
||||
factory $AsyncStateErrorCopyWith(
|
||||
AsyncStateError<T> value, $Res Function(AsyncStateError<T>) then) =
|
||||
_$AsyncStateErrorCopyWithImpl<T, $Res>;
|
||||
abstract class _$$AsyncStateErrorCopyWith<T, $Res> {
|
||||
factory _$$AsyncStateErrorCopyWith(_$AsyncStateError<T> value,
|
||||
$Res Function(_$AsyncStateError<T>) then) =
|
||||
__$$AsyncStateErrorCopyWithImpl<T, $Res>;
|
||||
$Res call({String errorTerm});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AsyncStateErrorCopyWithImpl<T, $Res>
|
||||
class __$$AsyncStateErrorCopyWithImpl<T, $Res>
|
||||
extends _$AsyncStateCopyWithImpl<T, $Res>
|
||||
implements $AsyncStateErrorCopyWith<T, $Res> {
|
||||
_$AsyncStateErrorCopyWithImpl(
|
||||
AsyncStateError<T> _value, $Res Function(AsyncStateError<T>) _then)
|
||||
: super(_value, (v) => _then(v as AsyncStateError<T>));
|
||||
implements _$$AsyncStateErrorCopyWith<T, $Res> {
|
||||
__$$AsyncStateErrorCopyWithImpl(
|
||||
_$AsyncStateError<T> _value, $Res Function(_$AsyncStateError<T>) _then)
|
||||
: super(_value, (v) => _then(v as _$AsyncStateError<T>));
|
||||
|
||||
@override
|
||||
AsyncStateError<T> get _value => super._value as AsyncStateError<T>;
|
||||
_$AsyncStateError<T> get _value => super._value as _$AsyncStateError<T>;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? errorTerm = freezed,
|
||||
}) {
|
||||
return _then(AsyncStateError<T>(
|
||||
return _then(_$AsyncStateError<T>(
|
||||
errorTerm == freezed
|
||||
? _value.errorTerm
|
||||
: errorTerm // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -592,7 +563,7 @@ class _$AsyncStateError<T>
|
|||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is AsyncStateError<T> &&
|
||||
other is _$AsyncStateError<T> &&
|
||||
const DeepCollectionEquality().equals(other.errorTerm, errorTerm));
|
||||
}
|
||||
|
||||
|
@ -602,8 +573,9 @@ class _$AsyncStateError<T>
|
|||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$AsyncStateErrorCopyWith<T, AsyncStateError<T>> get copyWith =>
|
||||
_$AsyncStateErrorCopyWithImpl<T, AsyncStateError<T>>(this, _$identity);
|
||||
_$$AsyncStateErrorCopyWith<T, _$AsyncStateError<T>> get copyWith =>
|
||||
__$$AsyncStateErrorCopyWithImpl<T, _$AsyncStateError<T>>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
|
@ -681,10 +653,10 @@ class _$AsyncStateError<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;
|
||||
@JsonKey(ignore: true)
|
||||
$AsyncStateErrorCopyWith<T, AsyncStateError<T>> get copyWith =>
|
||||
_$$AsyncStateErrorCopyWith<T, _$AsyncStateError<T>> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'async_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
|
||||
// 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 {
|
||||
Computed<bool>? _$isLoadingComputed;
|
||||
|
@ -23,7 +23,8 @@ mixin _$AsyncStore<T> on _AsyncStore<T>, Store {
|
|||
name: '_AsyncStore.errorTerm'))
|
||||
.value;
|
||||
|
||||
final _$asyncStateAtom = Atom(name: '_AsyncStore.asyncState');
|
||||
late final _$asyncStateAtom =
|
||||
Atom(name: '_AsyncStore.asyncState', context: context);
|
||||
|
||||
@override
|
||||
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
|
||||
Future<T?> run(AsyncValueGetter<T> callback, {bool refresh = false}) {
|
||||
return _$runAsyncAction.run(() => super.run(callback, refresh: refresh));
|
||||
}
|
||||
|
||||
final _$runLemmyAsyncAction = AsyncAction('_AsyncStore.runLemmy');
|
||||
late final _$runLemmyAsyncAction =
|
||||
AsyncAction('_AsyncStore.runLemmy', context: context);
|
||||
|
||||
@override
|
||||
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));
|
||||
}
|
||||
|
||||
final _$_AsyncStoreActionController = ActionController(name: '_AsyncStore');
|
||||
late final _$_AsyncStoreActionController =
|
||||
ActionController(name: '_AsyncStore', context: context);
|
||||
|
||||
@override
|
||||
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
|
||||
String toString() {
|
||||
return '''
|
||||
|
|
|
@ -12,32 +12,45 @@ class AsyncStoreListener<T> extends SingleChildStatelessWidget {
|
|||
T data,
|
||||
)? successMessageBuilder;
|
||||
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
T data,
|
||||
)? onSuccess;
|
||||
|
||||
const AsyncStoreListener({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.asyncStore,
|
||||
this.successMessageBuilder,
|
||||
Widget? child,
|
||||
}) : super(key: key, child: child);
|
||||
this.onSuccess,
|
||||
super.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget buildWithChild(BuildContext context, Widget? child) {
|
||||
return ObserverListener<AsyncStore<T>>(
|
||||
store: asyncStore,
|
||||
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) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(content: Text(errorTerm.tr(context))));
|
||||
} else if (store.asyncState is AsyncStateData &&
|
||||
(successMessageBuilder != null)) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(successMessageBuilder!(
|
||||
context, (store.asyncState as AsyncStateData).data))));
|
||||
}
|
||||
if (successMessageBuilder != null) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(successMessageBuilder!(context, data)),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: child ?? const SizedBox(),
|
||||
);
|
||||
|
|
|
@ -52,3 +52,14 @@ extension UserPreferredNames on PersonSafe {
|
|||
extension CommentLink on Comment {
|
||||
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/full_post/full_post.dart';
|
||||
import '../pages/instance.dart';
|
||||
import '../pages/media_view.dart';
|
||||
import '../pages/user.dart';
|
||||
|
||||
|
@ -25,9 +24,6 @@ Future<dynamic> goToReplace(
|
|||
builder: builder,
|
||||
));
|
||||
|
||||
void goToInstance(BuildContext context, String instanceHost) =>
|
||||
goTo(context, (context) => InstancePage(instanceHost: instanceHost));
|
||||
|
||||
// ignore: camel_case_types
|
||||
abstract class goToCommunity {
|
||||
/// Navigates to `CommunityPage`
|
||||
|
@ -63,7 +59,6 @@ void goToMedia(BuildContext context, String url) => Navigator.push(
|
|||
context,
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) => MediaViewPage(url),
|
||||
transitionDuration: const Duration(milliseconds: 300),
|
||||
opaque: false,
|
||||
transitionsBuilder: (_, animation, __, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
|
|
|
@ -8,34 +8,24 @@ import 'observer_consumers.dart';
|
|||
/// Important: this will not make [context.watch] react to changes
|
||||
class MobxProvider<T extends Store> extends Provider<T> {
|
||||
MobxProvider({
|
||||
Key? key,
|
||||
required Create<T> create,
|
||||
bool? lazy,
|
||||
TransitionBuilder? builder,
|
||||
Widget? child,
|
||||
super.key,
|
||||
required super.create,
|
||||
super.lazy,
|
||||
super.builder,
|
||||
super.child,
|
||||
}) : super(
|
||||
key: key,
|
||||
create: create,
|
||||
dispose: (context, store) {
|
||||
if (store is DisposableStore) store.dispose();
|
||||
},
|
||||
lazy: lazy,
|
||||
builder: builder,
|
||||
child: child,
|
||||
);
|
||||
|
||||
/// will not dispose the store
|
||||
MobxProvider.value({
|
||||
Key? key,
|
||||
required T value,
|
||||
TransitionBuilder? builder,
|
||||
Widget? child,
|
||||
}) : super.value(
|
||||
key: key,
|
||||
builder: builder,
|
||||
value: value,
|
||||
child: child,
|
||||
);
|
||||
super.key,
|
||||
required super.value,
|
||||
super.builder,
|
||||
super.child,
|
||||
}) : super.value();
|
||||
}
|
||||
|
||||
/// tracks reactions and disposes them in [DisposableStore.dispose]
|
||||
|
|
|
@ -14,10 +14,10 @@ class ObserverBuilder<T extends Store> extends StatelessWidget {
|
|||
final MobxBuilder<T> builder;
|
||||
|
||||
const ObserverBuilder({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.store,
|
||||
required this.builder,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -38,11 +38,11 @@ class ObserverListener<T extends Store> extends HookWidget {
|
|||
final Widget child;
|
||||
|
||||
const ObserverListener({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.store,
|
||||
required this.listener,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -64,11 +64,11 @@ class ObserverConsumer<T extends Store> extends HookWidget {
|
|||
final MobxBuilder<T> builder;
|
||||
|
||||
const ObserverConsumer({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.store,
|
||||
required this.listener,
|
||||
required this.builder,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
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(
|
||||
icon: const Icon(Icons.code),
|
||||
label: const Text('source code'),
|
||||
onPressed: () => openInBrowser(lemmurRepositoryLink),
|
||||
onPressed: () =>
|
||||
launchLink(link: lemmurRepositoryUrl, context: context),
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.monetization_on),
|
||||
|
@ -56,13 +57,17 @@ class AboutTile extends HookWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
openInBrowser('https://patreon.com/lemmur'),
|
||||
onPressed: () => launchLink(
|
||||
link: patreonUrl,
|
||||
context: context,
|
||||
),
|
||||
child: const Text('Patreon'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
openInBrowser('https://buymeacoff.ee/lemmur'),
|
||||
onPressed: () => launchLink(
|
||||
link: buyMeACoffeeUrl,
|
||||
context: context,
|
||||
),
|
||||
child: const Text('Buy Me a Coffee'),
|
||||
),
|
||||
],
|
||||
|
@ -91,7 +96,7 @@ class AboutTile extends HookWidget {
|
|||
class ChangelogPage extends StatelessWidget {
|
||||
final String changelog;
|
||||
|
||||
const ChangelogPage(this.changelog, {Key? key}) : super(key: key);
|
||||
const ChangelogPage(this.changelog, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -10,14 +10,14 @@ import 'cached_network_image.dart';
|
|||
/// Can be disabled with `noBlank`
|
||||
class Avatar extends HookWidget {
|
||||
const Avatar({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.url,
|
||||
this.radius = 25,
|
||||
this.noBlank = false,
|
||||
this.alwaysShow = false,
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final String? url;
|
||||
final double radius;
|
||||
|
|
|
@ -41,8 +41,8 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
this.width,
|
||||
this.fit,
|
||||
this.cache = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -9,7 +9,6 @@ import '../../l10n/l10n.dart';
|
|||
import '../../stores/config_store.dart';
|
||||
import '../../util/async_store_listener.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/extensions/cake_day.dart';
|
||||
import '../../util/goto.dart';
|
||||
import '../../util/mobx_provider.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
|
@ -36,8 +35,8 @@ class CommentWidget extends StatelessWidget {
|
|||
this.canBeMarkedAsRead = false,
|
||||
this.hideOnRead = false,
|
||||
this.userMentionId,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
CommentWidget.fromCommentView(
|
||||
CommentView cv, {
|
||||
|
|
|
@ -13,7 +13,7 @@ import 'comment_more_menu_button.dart';
|
|||
import 'comment_store.dart';
|
||||
|
||||
class CommentActions extends HookWidget {
|
||||
const CommentActions({Key? key}) : super(key: key);
|
||||
const CommentActions({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
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/logged_in_action.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../url_launcher.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/icons.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
|
@ -18,7 +18,7 @@ import 'comment.dart';
|
|||
import 'comment_store.dart';
|
||||
|
||||
class CommentMoreMenuButton extends HookWidget {
|
||||
const CommentMoreMenuButton({Key? key}) : super(key: key);
|
||||
const CommentMoreMenuButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -44,9 +44,8 @@ class _CommentMoreMenuPopup extends HookWidget {
|
|||
final CommentStore store;
|
||||
|
||||
const _CommentMoreMenuPopup({
|
||||
Key? key,
|
||||
required this.store,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -83,13 +82,7 @@ class _CommentMoreMenuPopup extends HookWidget {
|
|||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async {
|
||||
if (await ul.canLaunch(comment.link)) {
|
||||
await ul.launch(comment.link);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser")),
|
||||
);
|
||||
}
|
||||
await launchLink(link: comment.link, context: context);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
|
|
|
@ -54,6 +54,7 @@ abstract class _CommentStore with Store {
|
|||
_CommentStore(
|
||||
this._accountsStore, {
|
||||
required CommentTree commentTree,
|
||||
// ignore: unused_element
|
||||
this.userMentionId,
|
||||
required this.depth,
|
||||
required this.canBeMarkedAsRead,
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'comment_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
|
||||
// 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 {
|
||||
Computed<bool>? _$isMineComputed;
|
||||
|
@ -28,7 +28,8 @@ mixin _$CommentStore on _CommentStore, Store {
|
|||
Computed<bool>(() => super.isOP, name: '_CommentStore.isOP'))
|
||||
.value;
|
||||
|
||||
final _$commentAtom = Atom(name: '_CommentStore.comment');
|
||||
late final _$commentAtom =
|
||||
Atom(name: '_CommentStore.comment', context: context);
|
||||
|
||||
@override
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
Future<void> report(Jwt token, String reason) {
|
||||
return _$reportAsyncAction.run(() => super.report(token, reason));
|
||||
}
|
||||
|
||||
final _$deleteAsyncAction = AsyncAction('_CommentStore.delete');
|
||||
late final _$deleteAsyncAction =
|
||||
AsyncAction('_CommentStore.delete', context: context);
|
||||
|
||||
@override
|
||||
Future<void> delete(Jwt token) {
|
||||
return _$deleteAsyncAction.run(() => super.delete(token));
|
||||
}
|
||||
|
||||
final _$saveAsyncAction = AsyncAction('_CommentStore.save');
|
||||
late final _$saveAsyncAction =
|
||||
AsyncAction('_CommentStore.save', context: context);
|
||||
|
||||
@override
|
||||
Future<void> save(Jwt token) {
|
||||
return _$saveAsyncAction.run(() => super.save(token));
|
||||
}
|
||||
|
||||
final _$blockAsyncAction = AsyncAction('_CommentStore.block');
|
||||
late final _$blockAsyncAction =
|
||||
AsyncAction('_CommentStore.block', context: context);
|
||||
|
||||
@override
|
||||
Future<void> block(Jwt token) {
|
||||
return _$blockAsyncAction.run(() => super.block(token));
|
||||
}
|
||||
|
||||
final _$markAsReadAsyncAction = AsyncAction('_CommentStore.markAsRead');
|
||||
late final _$markAsReadAsyncAction =
|
||||
AsyncAction('_CommentStore.markAsRead', context: context);
|
||||
|
||||
@override
|
||||
Future<void> markAsRead(Jwt token) {
|
||||
return _$markAsReadAsyncAction.run(() => super.markAsRead(token));
|
||||
}
|
||||
|
||||
final _$_voteAsyncAction = AsyncAction('_CommentStore._vote');
|
||||
late final _$_voteAsyncAction =
|
||||
AsyncAction('_CommentStore._vote', context: context);
|
||||
|
||||
@override
|
||||
Future<void> _vote(VoteType voteType, Jwt token) {
|
||||
return _$_voteAsyncAction.run(() => super._vote(voteType, token));
|
||||
}
|
||||
|
||||
final _$upVoteAsyncAction = AsyncAction('_CommentStore.upVote');
|
||||
late final _$upVoteAsyncAction =
|
||||
AsyncAction('_CommentStore.upVote', context: context);
|
||||
|
||||
@override
|
||||
Future<void> upVote(Jwt token) {
|
||||
return _$upVoteAsyncAction.run(() => super.upVote(token));
|
||||
}
|
||||
|
||||
final _$downVoteAsyncAction = AsyncAction('_CommentStore.downVote');
|
||||
late final _$downVoteAsyncAction =
|
||||
AsyncAction('_CommentStore.downVote', context: context);
|
||||
|
||||
@override
|
||||
Future<void> downVote(Jwt token) {
|
||||
return _$downVoteAsyncAction.run(() => super.downVote(token));
|
||||
}
|
||||
|
||||
final _$_CommentStoreActionController =
|
||||
ActionController(name: '_CommentStore');
|
||||
late final _$_CommentStoreActionController =
|
||||
ActionController(name: '_CommentStore', context: context);
|
||||
|
||||
@override
|
||||
void toggleShowRaw() {
|
||||
|
|
|
@ -1,60 +1,87 @@
|
|||
import 'package:flutter/material.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
|
||||
class Editor extends HookWidget {
|
||||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final EditorController controller;
|
||||
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final int? minLines;
|
||||
final int? maxLines;
|
||||
final String? labelText;
|
||||
final String? initialValue;
|
||||
final bool autofocus;
|
||||
|
||||
/// Whether the editor should be preview the contents
|
||||
final bool fancy;
|
||||
final String instanceHost;
|
||||
|
||||
const Editor({
|
||||
Key? key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.onSubmitted,
|
||||
this.onChanged,
|
||||
this.minLines = 5,
|
||||
this.maxLines,
|
||||
this.labelText,
|
||||
this.initialValue,
|
||||
this.fancy = false,
|
||||
required this.instanceHost,
|
||||
this.autofocus = false,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultController = useTextEditingController();
|
||||
final actualController = controller ?? defaultController;
|
||||
|
||||
if (fancy) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: MarkdownText(
|
||||
actualController.text,
|
||||
instanceHost: instanceHost,
|
||||
controller.textEditingController.text,
|
||||
instanceHost: controller.instanceHost,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
controller: actualController,
|
||||
focusNode: focusNode,
|
||||
focusNode: controller.focusNode,
|
||||
controller: controller.textEditingController,
|
||||
autofocus: autofocus,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
onChanged: onChanged,
|
||||
onSubmitted: onSubmitted,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
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;
|
||||
|
||||
const FullscreenableImage({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.url,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
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`
|
||||
class InfiniteScroll<T> extends HookWidget {
|
||||
/// 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`,
|
||||
/// if the returned list has less than `batchSize` then the InfiniteScroll
|
||||
/// is considered finished
|
||||
final Future<List<T>> Function(int page, int batchSize) fetcher;
|
||||
final Fetcher<T> fetcher;
|
||||
|
||||
final InfiniteScrollController? controller;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../pages/community/community.dart';
|
||||
import '../../pages/instance/instance.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/goto.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
|
@ -67,9 +68,10 @@ class PostInfoSection extends StatelessWidget {
|
|||
text: post.post.originInstanceHost,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => goToInstance(
|
||||
context,
|
||||
post.post.originInstanceHost,
|
||||
..onTap = () => Navigator.of(context).push(
|
||||
InstancePage.route(
|
||||
post.post.originInstanceHost,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||
|
||||
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 '../../stores/accounts_store.dart';
|
||||
import '../../url_launcher.dart';
|
||||
import '../../util/icons.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../bottom_modal.dart';
|
||||
|
@ -72,10 +72,7 @@ class PostMoreMenu extends HookWidget {
|
|||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul.canLaunch(post.post.apId)
|
||||
? ul.launch(post.post.apId)
|
||||
: ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
onTap: () => launchLink(link: post.post.apId, context: context),
|
||||
),
|
||||
if (isMine)
|
||||
ListTile(
|
||||
|
|
|
@ -6,7 +6,7 @@ part of '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
|
||||
// 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 {
|
||||
Computed<String?>? _$urlDomainComputed;
|
||||
|
@ -23,7 +23,8 @@ mixin _$PostStore on _PostStore, Store {
|
|||
Computed<bool>(() => super.hasMedia, name: '_PostStore.hasMedia'))
|
||||
.value;
|
||||
|
||||
final _$postViewAtom = Atom(name: '_PostStore.postView');
|
||||
late final _$postViewAtom =
|
||||
Atom(name: '_PostStore.postView', context: context);
|
||||
|
||||
@override
|
||||
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
|
||||
Future<void> save(Jwt token) {
|
||||
return _$saveAsyncAction.run(() => super.save(token));
|
||||
}
|
||||
|
||||
final _$reportAsyncAction = AsyncAction('_PostStore.report');
|
||||
late final _$reportAsyncAction =
|
||||
AsyncAction('_PostStore.report', context: context);
|
||||
|
||||
@override
|
||||
Future<void> report(Jwt token, String reason) {
|
||||
return _$reportAsyncAction.run(() => super.report(token, reason));
|
||||
}
|
||||
|
||||
final _$blockUserAsyncAction = AsyncAction('_PostStore.blockUser');
|
||||
late final _$blockUserAsyncAction =
|
||||
AsyncAction('_PostStore.blockUser', context: context);
|
||||
|
||||
@override
|
||||
Future<void> blockUser(Jwt token) {
|
||||
return _$blockUserAsyncAction.run(() => super.blockUser(token));
|
||||
}
|
||||
|
||||
final _$_voteAsyncAction = AsyncAction('_PostStore._vote');
|
||||
late final _$_voteAsyncAction =
|
||||
AsyncAction('_PostStore._vote', context: context);
|
||||
|
||||
@override
|
||||
Future<void> _vote(Jwt token, VoteType voteType) {
|
||||
return _$_voteAsyncAction.run(() => super._vote(token, voteType));
|
||||
}
|
||||
|
||||
final _$_PostStoreActionController = ActionController(name: '_PostStore');
|
||||
late final _$_PostStoreActionController =
|
||||
ActionController(name: '_PostStore', context: context);
|
||||
|
||||
@override
|
||||
void updatePostView(PostView postView) {
|
||||
|
|
|
@ -6,10 +6,10 @@ class PullToRefresh extends StatelessWidget {
|
|||
final Widget child;
|
||||
|
||||
const PullToRefresh({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.onRefresh,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -20,7 +20,7 @@ class RadioPicker<T> extends StatelessWidget {
|
|||
final Widget? trailing;
|
||||
|
||||
const RadioPicker({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.values,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
|
@ -28,7 +28,7 @@ class RadioPicker<T> extends StatelessWidget {
|
|||
this.buttonBuilder,
|
||||
this.title,
|
||||
this.trailing,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -70,8 +70,8 @@ class SortableInfiniteList<T> extends HookWidget {
|
|||
|
||||
class InfinitePostList extends SortableInfiniteList<PostView> {
|
||||
InfinitePostList({
|
||||
required FetcherWithSorting<PostView> fetcher,
|
||||
InfiniteScrollController? controller,
|
||||
required super.fetcher,
|
||||
super.controller,
|
||||
}) : super(
|
||||
itemBuilder: (post) => Column(
|
||||
children: [
|
||||
|
@ -79,8 +79,6 @@ class InfinitePostList extends SortableInfiniteList<PostView> {
|
|||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
fetcher: fetcher,
|
||||
controller: controller,
|
||||
noItems: const Text('there are no posts'),
|
||||
uniqueProp: (item) => item.post.apId,
|
||||
);
|
||||
|
@ -88,15 +86,13 @@ class InfinitePostList extends SortableInfiniteList<PostView> {
|
|||
|
||||
class InfiniteCommentList extends SortableInfiniteList<CommentView> {
|
||||
InfiniteCommentList({
|
||||
required FetcherWithSorting<CommentView> fetcher,
|
||||
InfiniteScrollController? controller,
|
||||
required super.fetcher,
|
||||
super.controller,
|
||||
}) : super(
|
||||
itemBuilder: (comment) => CommentWidget(
|
||||
CommentTree(comment),
|
||||
detached: true,
|
||||
),
|
||||
fetcher: fetcher,
|
||||
controller: controller,
|
||||
noItems: const Text('there are no comments'),
|
||||
uniqueProp: (item) => item.comment.apId,
|
||||
);
|
||||
|
|
|
@ -14,14 +14,14 @@ class TileAction extends StatelessWidget {
|
|||
final Color? iconColor;
|
||||
|
||||
const TileAction({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.delayedLoading,
|
||||
this.iconColor,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
required this.tooltip,
|
||||
this.loading = false,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
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