mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2026-03-09 22:08:00 +01:00
Compare commits
33 Commits
rc1.12.0-1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4fcb5b0d9 | ||
|
|
1911004d05 | ||
|
|
5d67564445 | ||
|
|
be04c5a46e | ||
|
|
bd7a4c9dfb | ||
|
|
10ee57722e | ||
|
|
ff5f7ab50e | ||
|
|
277885a61e | ||
|
|
6633ebc376 | ||
|
|
b2d9986cd3 | ||
|
|
a0b9bb277f | ||
|
|
d381705cdd | ||
| 3820d4264a | |||
|
|
cf4e2d3fad | ||
|
|
002dc87577 | ||
|
|
922e7ad0ff | ||
|
|
5d7be8a672 | ||
|
|
431b357cfa | ||
|
|
2938acf152 | ||
|
|
4127f70e4d | ||
|
|
c07221cc12 | ||
|
|
33b2f95e3f | ||
|
|
2a5cd9b218 | ||
|
|
a15fed034d | ||
|
|
b9641ac021 | ||
|
|
2145bd8846 | ||
|
|
672b97b310 | ||
|
|
974da6ec90 | ||
|
|
f19bbcd010 | ||
|
|
a1468c92c8 | ||
|
|
6dd125a087 | ||
|
|
842ecc4235 | ||
|
|
db66793d28 |
@ -372,6 +372,7 @@ upload_linux_arm64:
|
|||||||
- tar czf package.tar.gz -C build/linux/arm64/release/bundle/ .
|
- tar czf package.tar.gz -C build/linux/arm64/release/bundle/ .
|
||||||
- |
|
- |
|
||||||
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-arm64.tar.gz
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-arm64.tar.gz
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
upload_windows:
|
upload_windows:
|
||||||
extends: .release
|
extends: .release
|
||||||
@ -383,6 +384,7 @@ upload_windows:
|
|||||||
- |
|
- |
|
||||||
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.zip ${PACKAGE_REGISTRY_URL}/fluffychat-windows.zip
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.zip ${PACKAGE_REGISTRY_URL}/fluffychat-windows.zip
|
||||||
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file fluffychat.msix ${PACKAGE_REGISTRY_URL}/fluffychat-windows.msix
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file fluffychat.msix ${PACKAGE_REGISTRY_URL}/fluffychat-windows.msix
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
deploy_playstore:
|
deploy_playstore:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 44 KiB |
@ -2252,9 +2252,9 @@
|
|||||||
"@hydrateTor": {},
|
"@hydrateTor": {},
|
||||||
"commandHint_googly": "أرسل بعض عيون googly",
|
"commandHint_googly": "أرسل بعض عيون googly",
|
||||||
"@commandHint_googly": {},
|
"@commandHint_googly": {},
|
||||||
"commandHint_cuddle": "إرسال عناق",
|
"commandHint_cuddle": "أرسل عناق",
|
||||||
"@commandHint_cuddle": {},
|
"@commandHint_cuddle": {},
|
||||||
"commandHint_hug": "إرسال عناق",
|
"commandHint_hug": "إرسال حضن",
|
||||||
"@commandHint_hug": {},
|
"@commandHint_hug": {},
|
||||||
"cuddleContent": "{senderName} يحتضنك",
|
"cuddleContent": "{senderName} يحتضنك",
|
||||||
"@cuddleContent": {
|
"@cuddleContent": {
|
||||||
@ -2504,5 +2504,11 @@
|
|||||||
"jumpToLastReadMessage": "الانتقال إلى آخر رسالة مقروءة",
|
"jumpToLastReadMessage": "الانتقال إلى آخر رسالة مقروءة",
|
||||||
"@jumpToLastReadMessage": {},
|
"@jumpToLastReadMessage": {},
|
||||||
"readUpToHere": "اقرأ حتى هنا",
|
"readUpToHere": "اقرأ حتى هنا",
|
||||||
"@readUpToHere": {}
|
"@readUpToHere": {},
|
||||||
|
"signInWithPassword": "سجل الدخول بكلمة السر",
|
||||||
|
"@signInWithPassword": {},
|
||||||
|
"continueWith": "أكمل ب:",
|
||||||
|
"@continueWith": {},
|
||||||
|
"pleaseTryAgainLaterOrChooseDifferentServer": "رجاء حاول مجددا أو اختر خادما مختلفا.",
|
||||||
|
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
|
||||||
}
|
}
|
||||||
|
|||||||
1
assets/l10n/intl_el.arb
Normal file
1
assets/l10n/intl_el.arb
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@ -2473,5 +2473,8 @@
|
|||||||
"jump": "Jump",
|
"jump": "Jump",
|
||||||
"openLinkInBrowser": "Open link in browser",
|
"openLinkInBrowser": "Open link in browser",
|
||||||
"reportErrorDescription": "Oh no. Something went wrong. Please try again later. If you want, you can report the bug to the developers.",
|
"reportErrorDescription": "Oh no. Something went wrong. Please try again later. If you want, you can report the bug to the developers.",
|
||||||
"report": "report"
|
"report": "report",
|
||||||
|
"signInWithPassword": "Sign in with password",
|
||||||
|
"continueWith": "Continue with:",
|
||||||
|
"pleaseTryAgainLaterOrChooseDifferentServer": "Please try again later or choose a different server."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2504,5 +2504,11 @@
|
|||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"reportErrorDescription": "Oh appike! Midagi läks valesti. Palun proovi hiljem uuesti. Kui soovid, võid sellest veast arendajatele teatada.",
|
"reportErrorDescription": "Oh appike! Midagi läks valesti. Palun proovi hiljem uuesti. Kui soovid, võid sellest veast arendajatele teatada.",
|
||||||
"@reportErrorDescription": {}
|
"@reportErrorDescription": {},
|
||||||
|
"continueWith": "Jätkamiseks kasuta:",
|
||||||
|
"@continueWith": {},
|
||||||
|
"signInWithPassword": "Logi sisse salasõnaga",
|
||||||
|
"@signInWithPassword": {},
|
||||||
|
"pleaseTryAgainLaterOrChooseDifferentServer": "Palun proovi hiljem uuesti või muuda serveri nime.",
|
||||||
|
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,7 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"activatedEndToEndEncryption": "🔐 {username} activou o cifrado extremo-a-extremo",
|
"activatedEndToEndEncryption": "🔐 {username} activou a cifraxe extremo-a-extremo",
|
||||||
"@activatedEndToEndEncryption": {
|
"@activatedEndToEndEncryption": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -314,7 +314,7 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"channelCorruptedDecryptError": "O cifrado está corrompido",
|
"channelCorruptedDecryptError": "A cifraxe está estragada",
|
||||||
"@channelCorruptedDecryptError": {
|
"@channelCorruptedDecryptError": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
@ -704,12 +704,12 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"enableEncryption": "Activar cifrado",
|
"enableEncryption": "Activar cifraxe",
|
||||||
"@enableEncryption": {
|
"@enableEncryption": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"enableEncryptionWarning": "Non poderás desactivar o cifrado posteriormente, tes certeza?",
|
"enableEncryptionWarning": "Non poderás desactivar a cifraxe posteriormente, tes certeza?",
|
||||||
"@enableEncryptionWarning": {
|
"@enableEncryptionWarning": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
@ -719,12 +719,12 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"encryption": "Cifrado",
|
"encryption": "Cifraxe",
|
||||||
"@encryption": {
|
"@encryption": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"encryptionNotEnabled": "O cifrado non está activado",
|
"encryptionNotEnabled": "A cifraxe non está activada",
|
||||||
"@encryptionNotEnabled": {
|
"@encryptionNotEnabled": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
@ -1119,7 +1119,7 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"needPantalaimonWarning": "Ten en conta que polo de agora precisas Pantalaimon para o cifrado extremo-a-extremo.",
|
"needPantalaimonWarning": "Ten en conta que polo de agora precisas Pantalaimon para a cifraxe extremo-a-extremo.",
|
||||||
"@needPantalaimonWarning": {
|
"@needPantalaimonWarning": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
@ -1159,7 +1159,7 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"noEncryptionForPublicRooms": "Só podes activar o cifrado tan pronto como a sala non sexa públicamente accesible.",
|
"noEncryptionForPublicRooms": "Só podes activar a cifraxe tan pronto como a sala non sexa públicamente accesible.",
|
||||||
"@noEncryptionForPublicRooms": {
|
"@noEncryptionForPublicRooms": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
@ -1824,7 +1824,7 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"unknownEncryptionAlgorithm": "Algoritmo de cifrado descoñecido",
|
"unknownEncryptionAlgorithm": "Algoritmo de cifraxe descoñecido",
|
||||||
"@unknownEncryptionAlgorithm": {
|
"@unknownEncryptionAlgorithm": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
@ -2103,7 +2103,7 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"description": "Usage hint for the command /discardsession"
|
"description": "Usage hint for the command /discardsession"
|
||||||
},
|
},
|
||||||
"commandHint_create": "Crear un grupo de conversa baleiro\nUsa --no-encryption para desactivar o cifrado",
|
"commandHint_create": "Crear un grupo de conversa baleiro\nUsa --no-encryption para desactivar a cifraxe",
|
||||||
"@commandHint_create": {
|
"@commandHint_create": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"description": "Usage hint for the command /create"
|
"description": "Usage hint for the command /create"
|
||||||
@ -2113,7 +2113,7 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"description": "Usage hint for the command /clearcache"
|
"description": "Usage hint for the command /clearcache"
|
||||||
},
|
},
|
||||||
"commandHint_dm": "Iniciar un chat directo\nUsa --no-encryption para desactivar o cifrado",
|
"commandHint_dm": "Iniciar un chat directo\nUsa --no-encryption para desactivar a cifraxe",
|
||||||
"@commandHint_dm": {
|
"@commandHint_dm": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"description": "Usage hint for the command /dm"
|
"description": "Usage hint for the command /dm"
|
||||||
@ -2447,9 +2447,9 @@
|
|||||||
"@enterInviteLinkOrMatrixId": {},
|
"@enterInviteLinkOrMatrixId": {},
|
||||||
"encryptThisChat": "Cifrar esta conversa",
|
"encryptThisChat": "Cifrar esta conversa",
|
||||||
"@encryptThisChat": {},
|
"@encryptThisChat": {},
|
||||||
"endToEndEncryption": "Cifrado de extremo a extremo",
|
"endToEndEncryption": "Cifraxe de extremo a extremo",
|
||||||
"@endToEndEncryption": {},
|
"@endToEndEncryption": {},
|
||||||
"disableEncryptionWarning": "Por razóns de seguridade non podes desactivar o cifrado dunha conversa onde foi activado previamente.",
|
"disableEncryptionWarning": "Por razóns de seguridade non podes desactivar a cifraxe dunha conversa onde foi activada previamente.",
|
||||||
"@disableEncryptionWarning": {},
|
"@disableEncryptionWarning": {},
|
||||||
"sorryThatsNotPossible": "Lamentámolo... iso non é posible",
|
"sorryThatsNotPossible": "Lamentámolo... iso non é posible",
|
||||||
"@sorryThatsNotPossible": {},
|
"@sorryThatsNotPossible": {},
|
||||||
@ -2504,5 +2504,11 @@
|
|||||||
"@discover": {
|
"@discover": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
}
|
},
|
||||||
|
"signInWithPassword": "Accede con contrasinal",
|
||||||
|
"@signInWithPassword": {},
|
||||||
|
"continueWith": "Continuar con:",
|
||||||
|
"@continueWith": {},
|
||||||
|
"pleaseTryAgainLaterOrChooseDifferentServer": "Inténtao máis tarde ou elixe un servidor diferente.",
|
||||||
|
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2503,6 +2503,12 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"reportErrorDescription": "Dogodila se greška. Pokušaj ponovo kasnije. Ako želiš grešku možeš prijaviti programerima.",
|
"reportErrorDescription": "Dogodila se greška. Pokušaj ponovo kasnije. Ako želiš, grešku možeš prijaviti programerima.",
|
||||||
"@reportErrorDescription": {}
|
"@reportErrorDescription": {},
|
||||||
|
"signInWithPassword": "Prijavi se s lozinkom",
|
||||||
|
"@signInWithPassword": {},
|
||||||
|
"continueWith": "Nastavi sa:",
|
||||||
|
"@continueWith": {},
|
||||||
|
"pleaseTryAgainLaterOrChooseDifferentServer": "Pokušaj ponovo kasnije ili odaberi jedan drugi poslužitelj.",
|
||||||
|
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2503,5 +2503,11 @@
|
|||||||
"report": "laporkan",
|
"report": "laporkan",
|
||||||
"@report": {},
|
"@report": {},
|
||||||
"reportErrorDescription": "Aduh. Ada yang salah. Silakan coba lahi nanti. Jika kamu mau, kamu bisa melaporkan kutu ini kepada para pengembang.",
|
"reportErrorDescription": "Aduh. Ada yang salah. Silakan coba lahi nanti. Jika kamu mau, kamu bisa melaporkan kutu ini kepada para pengembang.",
|
||||||
"@reportErrorDescription": {}
|
"@reportErrorDescription": {},
|
||||||
|
"signInWithPassword": "Masuk dengan kata sandi",
|
||||||
|
"@signInWithPassword": {},
|
||||||
|
"continueWith": "Lanjutkan dengan:",
|
||||||
|
"@continueWith": {},
|
||||||
|
"pleaseTryAgainLaterOrChooseDifferentServer": "Silakan coba lagi nanti atau pilih server yang lain.",
|
||||||
|
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2496,5 +2496,11 @@
|
|||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"reopenChat": "Deschide din nou chatul",
|
"reopenChat": "Deschide din nou chatul",
|
||||||
"@reopenChat": {}
|
"@reopenChat": {},
|
||||||
|
"continueWith": "Continuați cu:",
|
||||||
|
"@continueWith": {},
|
||||||
|
"pleaseTryAgainLaterOrChooseDifferentServer": "Vă rugăm să încercați din nou mai târziu sau să alegeți un server diferit.",
|
||||||
|
"@pleaseTryAgainLaterOrChooseDifferentServer": {},
|
||||||
|
"signInWithPassword": "Conectați-vă cu parolă",
|
||||||
|
"@signInWithPassword": {}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2504,5 +2504,11 @@
|
|||||||
"@discover": {
|
"@discover": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
}
|
},
|
||||||
|
"pleaseTryAgainLaterOrChooseDifferentServer": "Спробуйте пізніше або виберіть інший сервер.",
|
||||||
|
"@pleaseTryAgainLaterOrChooseDifferentServer": {},
|
||||||
|
"signInWithPassword": "Увійти за допомогою пароля",
|
||||||
|
"@signInWithPassword": {},
|
||||||
|
"continueWith": "Продовжити за допомогою:",
|
||||||
|
"@continueWith": {}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 146 KiB |
@ -55,7 +55,7 @@
|
|||||||
<div class="flex mb-8 justify-center content-center">
|
<div class="flex mb-8 justify-center content-center">
|
||||||
<a rel="me"
|
<a rel="me"
|
||||||
class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
|
class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
|
||||||
rel="me" href="https://metalhead.club/@krille">
|
rel="me" href="https://mastodon.art/@krille">
|
||||||
<svg class="fill-current h-6" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
<svg class="fill-current h-6" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
|
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
|
||||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||||
@ -116,4 +116,4 @@
|
|||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -9,7 +9,6 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
|||||||
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
|
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||||
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
|
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
|
||||||
import 'package:fluffychat/pages/connect/connect_page.dart';
|
|
||||||
import 'package:fluffychat/pages/device_settings/device_settings.dart';
|
import 'package:fluffychat/pages/device_settings/device_settings.dart';
|
||||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
||||||
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
|
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
|
||||||
@ -27,7 +26,6 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d
|
|||||||
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
||||||
import 'package:fluffychat/pages/settings_stories/settings_stories.dart';
|
import 'package:fluffychat/pages/settings_stories/settings_stories.dart';
|
||||||
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
||||||
import 'package:fluffychat/pages/sign_up/signup.dart';
|
|
||||||
import 'package:fluffychat/pages/story/story_page.dart';
|
import 'package:fluffychat/pages/story/story_page.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/loading_view.dart';
|
import 'package:fluffychat/widgets/layouts/loading_view.dart';
|
||||||
@ -266,23 +264,6 @@ class AppRoutes {
|
|||||||
widget: const Login(),
|
widget: const Login(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
|
||||||
path: 'connect',
|
|
||||||
widget: const ConnectPage(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
stackedRoutes: [
|
|
||||||
VWidget(
|
|
||||||
path: 'login',
|
|
||||||
widget: const Login(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
VWidget(
|
|
||||||
path: 'signup',
|
|
||||||
widget: const SignupPage(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
widget: const LogViewer(),
|
widget: const LogViewer(),
|
||||||
@ -358,23 +339,6 @@ class AppRoutes {
|
|||||||
widget: const Login(),
|
widget: const Login(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
|
||||||
path: 'connect',
|
|
||||||
widget: const ConnectPage(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
stackedRoutes: [
|
|
||||||
VWidget(
|
|
||||||
path: 'login',
|
|
||||||
widget: const Login(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
VWidget(
|
|
||||||
path: 'signup',
|
|
||||||
widget: const SignupPage(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
|
|||||||
@ -41,6 +41,22 @@ abstract class FluffyThemes {
|
|||||||
titleSmall: fallbackTextStyle,
|
titleSmall: fallbackTextStyle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static LinearGradient backgroundGradient(
|
||||||
|
BuildContext context,
|
||||||
|
int alpha,
|
||||||
|
) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
colorScheme.primaryContainer.withAlpha(alpha),
|
||||||
|
colorScheme.secondaryContainer.withAlpha(alpha),
|
||||||
|
colorScheme.tertiaryContainer.withAlpha(alpha),
|
||||||
|
colorScheme.primaryContainer.withAlpha(alpha),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static const Duration animationDuration = Duration(milliseconds: 250);
|
static const Duration animationDuration = Duration(milliseconds: 250);
|
||||||
static const Curve animationCurve = Curves.easeInOut;
|
static const Curve animationCurve = Curves.easeInOut;
|
||||||
|
|
||||||
|
|||||||
@ -204,6 +204,7 @@ class ChatController extends State<ChatPageWithRoom> {
|
|||||||
|
|
||||||
void requestHistory() async {
|
void requestHistory() async {
|
||||||
if (!timeline!.canRequestHistory) return;
|
if (!timeline!.canRequestHistory) return;
|
||||||
|
Logs().v('Requesting history...');
|
||||||
try {
|
try {
|
||||||
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -222,6 +223,7 @@ class ChatController extends State<ChatPageWithRoom> {
|
|||||||
final timeline = this.timeline;
|
final timeline = this.timeline;
|
||||||
if (timeline == null) return;
|
if (timeline == null) return;
|
||||||
if (!timeline.canRequestFuture) return;
|
if (!timeline.canRequestFuture) return;
|
||||||
|
Logs().v('Requesting future...');
|
||||||
try {
|
try {
|
||||||
final mostRecentEventId = timeline.events.first.eventId;
|
final mostRecentEventId = timeline.events.first.eventId;
|
||||||
await timeline.requestFuture(historyCount: _loadHistoryCount);
|
await timeline.requestFuture(historyCount: _loadHistoryCount);
|
||||||
@ -244,12 +246,6 @@ class ChatController extends State<ChatPageWithRoom> {
|
|||||||
}
|
}
|
||||||
setReadMarker();
|
setReadMarker();
|
||||||
if (!scrollController.hasClients) return;
|
if (!scrollController.hasClients) return;
|
||||||
if (scrollController.position.pixels ==
|
|
||||||
scrollController.position.maxScrollExtent) {
|
|
||||||
requestHistory();
|
|
||||||
} else if (scrollController.position.pixels == 0) {
|
|
||||||
requestFuture();
|
|
||||||
}
|
|
||||||
if (timeline?.allowNewEvent == false ||
|
if (timeline?.allowNewEvent == false ||
|
||||||
scrollController.position.pixels > 0 && _scrolledUp == false) {
|
scrollController.position.pixels > 0 && _scrolledUp == false) {
|
||||||
setState(() => _scrolledUp = true);
|
setState(() => _scrolledUp = true);
|
||||||
@ -873,8 +869,11 @@ class ChatController extends State<ChatPageWithRoom> {
|
|||||||
setState(() => showEmojiPicker = false);
|
setState(() => showEmojiPicker = false);
|
||||||
if (emoji == null) return;
|
if (emoji == null) return;
|
||||||
// make sure we don't send the same emoji twice
|
// make sure we don't send the same emoji twice
|
||||||
if (_allReactionEvents
|
if (_allReactionEvents.any(
|
||||||
.any((e) => e.content['m.relates_to']['key'] == emoji.emoji)) return;
|
(e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji.emoji,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return sendEmojiAction(emoji.emoji);
|
return sendEmojiAction(emoji.emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,11 +53,18 @@ class ChatEventList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (controller.timeline!.canRequestFuture) {
|
if (controller.timeline!.canRequestFuture) {
|
||||||
return Center(
|
return Builder(
|
||||||
child: IconButton(
|
builder: (context) {
|
||||||
onPressed: controller.requestFuture,
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
icon: const Icon(Icons.refresh_outlined),
|
(_) => controller.requestFuture(),
|
||||||
),
|
);
|
||||||
|
return Center(
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: controller.requestFuture,
|
||||||
|
icon: const Icon(Icons.refresh_outlined),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
@ -77,11 +84,18 @@ class ChatEventList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (controller.timeline!.canRequestHistory) {
|
if (controller.timeline!.canRequestHistory) {
|
||||||
return Center(
|
return Builder(
|
||||||
child: IconButton(
|
builder: (context) {
|
||||||
onPressed: controller.requestHistory,
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
icon: const Icon(Icons.refresh_outlined),
|
(_) => controller.requestHistory(),
|
||||||
),
|
);
|
||||||
|
return Center(
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: controller.requestHistory,
|
||||||
|
icon: const Icon(Icons.refresh_outlined),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
|
|||||||
@ -146,7 +146,6 @@ class ChatView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
|
||||||
|
|
||||||
return VWidgetGuard(
|
return VWidgetGuard(
|
||||||
onSystemPop: (redirector) async {
|
onSystemPop: (redirector) async {
|
||||||
@ -220,14 +219,9 @@ class ChatView extends StatelessWidget {
|
|||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: FluffyThemes.backgroundGradient(
|
||||||
begin: Alignment.topCenter,
|
context,
|
||||||
colors: [
|
64,
|
||||||
colorScheme.primaryContainer.withAlpha(64),
|
|
||||||
colorScheme.secondaryContainer.withAlpha(64),
|
|
||||||
colorScheme.tertiaryContainer.withAlpha(64),
|
|
||||||
colorScheme.primaryContainer.withAlpha(64),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -36,19 +36,16 @@ class _CuteContentState extends State<CuteContent> {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: addOverlay,
|
onTap: addOverlay,
|
||||||
child: SizedBox.square(
|
child: Column(
|
||||||
dimension: 300,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Text(
|
||||||
children: [
|
widget.event.text,
|
||||||
Text(
|
style: const TextStyle(fontSize: 150),
|
||||||
widget.event.text,
|
),
|
||||||
style: const TextStyle(fontSize: 150),
|
if (label != null) Text(label)
|
||||||
),
|
],
|
||||||
if (label != null) Text(label)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -144,24 +141,26 @@ class _CuteEventOverlayState extends State<CuteEventOverlay>
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: constraints.maxHeight,
|
height: constraints.maxHeight,
|
||||||
width: constraints.maxWidth,
|
width: constraints.maxWidth,
|
||||||
child: Stack(
|
child: OverflowBox(
|
||||||
alignment: Alignment.bottomLeft,
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
alignment: Alignment.bottomLeft,
|
||||||
children: items
|
fit: StackFit.expand,
|
||||||
.map(
|
children: items
|
||||||
(position) => Positioned(
|
.map(
|
||||||
left: position.width * width,
|
(position) => Positioned(
|
||||||
bottom: (height *
|
left: position.width * width,
|
||||||
.25 *
|
bottom: (height *
|
||||||
position.height *
|
.25 *
|
||||||
(controller?.value ?? 0)) -
|
position.height *
|
||||||
_CuteOverlayContent.size,
|
(controller?.value ?? 0)) -
|
||||||
child: _CuteOverlayContent(
|
_CuteOverlayContent.size,
|
||||||
emoji: widget.emoji,
|
child: _CuteOverlayContent(
|
||||||
|
emoji: widget.emoji,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
.toList(),
|
||||||
.toList(),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -61,7 +61,7 @@ class MessageReactions extends StatelessWidget {
|
|||||||
final evt = allReactionEvents.firstWhereOrNull(
|
final evt = allReactionEvents.firstWhereOrNull(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.senderId == e.room.client.userID &&
|
e.senderId == e.room.client.userID &&
|
||||||
e.content['m.relates_to']['key'] == r.key,
|
e.content.tryGetMap('m.relates_to')?['key'] == r.key,
|
||||||
);
|
);
|
||||||
if (evt != null) {
|
if (evt != null) {
|
||||||
showFutureLoadingDialog(
|
showFutureLoadingDialog(
|
||||||
|
|||||||
@ -52,7 +52,8 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||||||
final networkUri = _networkUri;
|
final networkUri = _networkUri;
|
||||||
if (kIsWeb && networkUri != null && _chewieManager == null) {
|
if (kIsWeb && networkUri != null && _chewieManager == null) {
|
||||||
_chewieManager ??= ChewieController(
|
_chewieManager ??= ChewieController(
|
||||||
videoPlayerController: VideoPlayerController.network(networkUri),
|
videoPlayerController:
|
||||||
|
VideoPlayerController.networkUrl(Uri.parse(networkUri)),
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
autoInitialize: true,
|
autoInitialize: true,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -183,12 +183,13 @@ class InputBar extends StatelessWidget {
|
|||||||
final state = r.getState(EventTypes.RoomCanonicalAlias);
|
final state = r.getState(EventTypes.RoomCanonicalAlias);
|
||||||
if ((state != null &&
|
if ((state != null &&
|
||||||
((state.content['alias'] is String &&
|
((state.content['alias'] is String &&
|
||||||
state.content['alias']
|
state.content
|
||||||
|
.tryGet<String>('alias')!
|
||||||
.split(':')[0]
|
.split(':')[0]
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.contains(roomSearch)) ||
|
.contains(roomSearch)) ||
|
||||||
(state.content['alt_aliases'] is List &&
|
(state.content['alt_aliases'] is List &&
|
||||||
state.content['alt_aliases'].any(
|
(state.content['alt_aliases'] as List).any(
|
||||||
(l) =>
|
(l) =>
|
||||||
l is String &&
|
l is String &&
|
||||||
l
|
l
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class ReactionsPicker extends StatelessWidget {
|
|||||||
|
|
||||||
for (final event in allReactionEvents) {
|
for (final event in allReactionEvents) {
|
||||||
try {
|
try {
|
||||||
emojis.remove(event.content['m.relates_to']['key']);
|
emojis.remove(event.content.tryGetMap('m.relates_to')!['key']);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
return Row(
|
return Row(
|
||||||
|
|||||||
@ -83,7 +83,9 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||||||
RequestType.GET,
|
RequestType.GET,
|
||||||
'/client/unstable/org.matrix.msc2432/rooms/${Uri.encodeComponent(room.id)}/aliases',
|
'/client/unstable/org.matrix.msc2432/rooms/${Uri.encodeComponent(room.id)}/aliases',
|
||||||
)
|
)
|
||||||
.then((response) => List<String>.from(response['aliases'])),
|
.then(
|
||||||
|
(response) => List<String>.from(response['aliases'] as Iterable),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
// Switch to the stable api once it is implemented.
|
// Switch to the stable api once it is implemented.
|
||||||
|
|
||||||
|
|||||||
@ -73,8 +73,10 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
|||||||
|
|
||||||
void updateRoomAction(Capabilities capabilities) async {
|
void updateRoomAction(Capabilities capabilities) async {
|
||||||
final room = Matrix.of(context).client.getRoomById(roomId!)!;
|
final room = Matrix.of(context).client.getRoomById(roomId!)!;
|
||||||
final String roomVersion =
|
final roomVersion = room
|
||||||
room.getState(EventTypes.RoomCreate)!.content['room_version'] ?? '1';
|
.getState(EventTypes.RoomCreate)!
|
||||||
|
.content['room_version'] as String? ??
|
||||||
|
'1';
|
||||||
final newVersion = await showConfirmationDialog<String>(
|
final newVersion = await showConfirmationDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.replaceRoomWithNewerVersion,
|
title: L10n.of(context)!.replaceRoomWithNewerVersion,
|
||||||
|
|||||||
@ -127,9 +127,9 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final String roomVersion = room
|
final roomVersion = room
|
||||||
.getState(EventTypes.RoomCreate)!
|
.getState(EventTypes.RoomCreate)!
|
||||||
.content['room_version'] ??
|
.content['room_version'] as String? ??
|
||||||
'1';
|
'1';
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
|||||||
@ -1,197 +0,0 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
|
||||||
import 'package:universal_html/html.dart' as html;
|
|
||||||
import 'package:vrouter/vrouter.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
|
||||||
import 'package:fluffychat/pages/connect/connect_page_view.dart';
|
|
||||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
|
||||||
|
|
||||||
class ConnectPage extends StatefulWidget {
|
|
||||||
const ConnectPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConnectPage> createState() => ConnectPageController();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConnectPageController extends State<ConnectPage> {
|
|
||||||
final TextEditingController usernameController = TextEditingController();
|
|
||||||
String? signupError;
|
|
||||||
bool loading = false;
|
|
||||||
|
|
||||||
void pickAvatar() async {
|
|
||||||
final source = !PlatformInfos.isMobile
|
|
||||||
? ImageSource.gallery
|
|
||||||
: await showModalActionSheet<ImageSource>(
|
|
||||||
context: context,
|
|
||||||
title: L10n.of(context)!.changeYourAvatar,
|
|
||||||
actions: [
|
|
||||||
SheetAction(
|
|
||||||
key: ImageSource.camera,
|
|
||||||
label: L10n.of(context)!.openCamera,
|
|
||||||
isDefaultAction: true,
|
|
||||||
icon: Icons.camera_alt_outlined,
|
|
||||||
),
|
|
||||||
SheetAction(
|
|
||||||
key: ImageSource.gallery,
|
|
||||||
label: L10n.of(context)!.openGallery,
|
|
||||||
icon: Icons.photo_outlined,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (source == null) return;
|
|
||||||
final picked = await ImagePicker().pickImage(
|
|
||||||
source: source,
|
|
||||||
imageQuality: 50,
|
|
||||||
maxWidth: 512,
|
|
||||||
maxHeight: 512,
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
Matrix.of(context).loginAvatar = picked;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void signUp() async {
|
|
||||||
usernameController.text = usernameController.text.trim();
|
|
||||||
final localpart =
|
|
||||||
usernameController.text.toLowerCase().replaceAll(' ', '_');
|
|
||||||
if (localpart.isEmpty) {
|
|
||||||
setState(() {
|
|
||||||
signupError = L10n.of(context)!.pleaseChooseAUsername;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
signupError = null;
|
|
||||||
loading = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
await Matrix.of(context).getLoginClient().register(username: localpart);
|
|
||||||
} on MatrixException catch (e) {
|
|
||||||
if (!e.requireAdditionalAuthentication) rethrow;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
Matrix.of(context).loginUsername = usernameController.text;
|
|
||||||
VRouter.of(context).to('signup');
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().d('Sign up failed', e, s);
|
|
||||||
setState(() {
|
|
||||||
signupError = e.toLocalizedString(context);
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _supportsFlow(String flowType) =>
|
|
||||||
Matrix.of(context)
|
|
||||||
.loginHomeserverSummary
|
|
||||||
?.loginFlows
|
|
||||||
.any((flow) => flow.type == flowType) ??
|
|
||||||
false;
|
|
||||||
|
|
||||||
bool get supportsSso => _supportsFlow('m.login.sso');
|
|
||||||
|
|
||||||
bool isDefaultPlatform =
|
|
||||||
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS);
|
|
||||||
|
|
||||||
bool get supportsLogin => _supportsFlow('m.login.password');
|
|
||||||
|
|
||||||
void login() => VRouter.of(context).to('login');
|
|
||||||
|
|
||||||
Map<String, dynamic>? _rawLoginTypes;
|
|
||||||
|
|
||||||
List<IdentityProvider>? get identityProviders {
|
|
||||||
final loginTypes = _rawLoginTypes;
|
|
||||||
if (loginTypes == null) return null;
|
|
||||||
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
|
|
||||||
(flow) => flow['type'] == AuthenticationTypes.sso,
|
|
||||||
)['identity_providers'];
|
|
||||||
final list = (rawProviders as List)
|
|
||||||
.map((json) => IdentityProvider.fromJson(json))
|
|
||||||
.toList();
|
|
||||||
if (PlatformInfos.isCupertinoStyle) {
|
|
||||||
list.sort((a, b) => a.brand == 'apple' ? -1 : 1);
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ssoLoginAction(String id) async {
|
|
||||||
final redirectUrl = kIsWeb
|
|
||||||
? '${html.window.origin!}/web/auth.html'
|
|
||||||
: isDefaultPlatform
|
|
||||||
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
|
|
||||||
: 'http://localhost:3001//login';
|
|
||||||
final url =
|
|
||||||
'${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
|
|
||||||
final urlScheme = isDefaultPlatform
|
|
||||||
? Uri.parse(redirectUrl).scheme
|
|
||||||
: "http://localhost:3001";
|
|
||||||
final result = await FlutterWebAuth2.authenticate(
|
|
||||||
url: url,
|
|
||||||
callbackUrlScheme: urlScheme,
|
|
||||||
);
|
|
||||||
final token = Uri.parse(result).queryParameters['loginToken'];
|
|
||||||
if (token?.isEmpty ?? false) return;
|
|
||||||
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => Matrix.of(context).getLoginClient().login(
|
|
||||||
LoginType.mLoginToken,
|
|
||||||
token: token,
|
|
||||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
if (supportsSso) {
|
|
||||||
Matrix.of(context)
|
|
||||||
.getLoginClient()
|
|
||||||
.request(
|
|
||||||
RequestType.GET,
|
|
||||||
'/client/r0/login',
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
(loginTypes) => setState(() {
|
|
||||||
_rawLoginTypes = loginTypes;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => ConnectPageView(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
class IdentityProvider {
|
|
||||||
final String? id;
|
|
||||||
final String? name;
|
|
||||||
final String? icon;
|
|
||||||
final String? brand;
|
|
||||||
|
|
||||||
IdentityProvider({this.id, this.name, this.icon, this.brand});
|
|
||||||
|
|
||||||
factory IdentityProvider.fromJson(Map<String, dynamic> json) =>
|
|
||||||
IdentityProvider(
|
|
||||||
id: json['id'],
|
|
||||||
name: json['name'],
|
|
||||||
icon: json['icon'],
|
|
||||||
brand: json['brand'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/pages/connect/connect_page.dart';
|
|
||||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
|
||||||
import 'sso_button.dart';
|
|
||||||
|
|
||||||
class ConnectPageView extends StatelessWidget {
|
|
||||||
final ConnectPageController controller;
|
|
||||||
const ConnectPageView(this.controller, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final avatar = Matrix.of(context).loginAvatar;
|
|
||||||
final identityProviders = controller.identityProviders;
|
|
||||||
return LoginScaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: controller.loading ? null : const BackButton(),
|
|
||||||
automaticallyImplyLeading: !controller.loading,
|
|
||||||
centerTitle: true,
|
|
||||||
title: Text(
|
|
||||||
Matrix.of(context).getLoginClient().homeserver?.host ?? '',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
key: const Key('ConnectPageListView'),
|
|
||||||
children: [
|
|
||||||
if (Matrix.of(context).loginRegistrationSupported ?? false) ...[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Center(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Material(
|
|
||||||
borderRadius: BorderRadius.circular(64),
|
|
||||||
elevation: Theme.of(context)
|
|
||||||
.appBarTheme
|
|
||||||
.scrolledUnderElevation ??
|
|
||||||
10,
|
|
||||||
color: Colors.transparent,
|
|
||||||
shadowColor: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onBackground
|
|
||||||
.withAlpha(64),
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 64,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
child: avatar == null
|
|
||||||
? const Icon(
|
|
||||||
Icons.person,
|
|
||||||
color: Colors.black,
|
|
||||||
size: 64,
|
|
||||||
)
|
|
||||||
: FutureBuilder<Uint8List>(
|
|
||||||
future: avatar.readAsBytes(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final bytes = snapshot.data;
|
|
||||||
if (bytes == null) {
|
|
||||||
return const CircularProgressIndicator
|
|
||||||
.adaptive();
|
|
||||||
}
|
|
||||||
return Image.memory(
|
|
||||||
bytes,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
width: 128,
|
|
||||||
height: 128,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
mini: true,
|
|
||||||
onPressed: controller.pickAvatar,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: Colors.black,
|
|
||||||
child: const Icon(Icons.camera_alt_outlined),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: TextField(
|
|
||||||
controller: controller.usernameController,
|
|
||||||
onSubmitted: (_) => controller.signUp(),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.account_box_outlined),
|
|
||||||
hintText: L10n.of(context)!.chooseAUsername,
|
|
||||||
errorText: controller.signupError,
|
|
||||||
errorStyle: const TextStyle(color: Colors.orange),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Hero(
|
|
||||||
tag: 'loginButton',
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
onPressed: controller.loading ? () {} : controller.signUp,
|
|
||||||
icon: const Icon(Icons.person_add_outlined),
|
|
||||||
label: controller.loading
|
|
||||||
? const LinearProgressIndicator()
|
|
||||||
: Text(L10n.of(context)!.signUp),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Divider(
|
|
||||||
thickness: 1,
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Text(
|
|
||||||
L10n.of(context)!.or,
|
|
||||||
style: const TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Divider(
|
|
||||||
thickness: 1,
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (controller.supportsSso)
|
|
||||||
identityProviders == null
|
|
||||||
? const SizedBox(
|
|
||||||
height: 74,
|
|
||||||
child: Center(child: CircularProgressIndicator.adaptive()),
|
|
||||||
)
|
|
||||||
: Center(
|
|
||||||
child: identityProviders.length == 1
|
|
||||||
? Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primaryContainer,
|
|
||||||
foregroundColor: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
icon: identityProviders.single.icon == null
|
|
||||||
? const Icon(
|
|
||||||
Icons.web_outlined,
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
: Image.network(
|
|
||||||
Uri.parse(identityProviders.single.icon!)
|
|
||||||
.getDownloadLink(
|
|
||||||
Matrix.of(context).getLoginClient(),
|
|
||||||
)
|
|
||||||
.toString(),
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
onPressed: () => controller
|
|
||||||
.ssoLoginAction(identityProviders.single.id!),
|
|
||||||
label: Text(
|
|
||||||
identityProviders.single.name ??
|
|
||||||
identityProviders.single.brand ??
|
|
||||||
L10n.of(context)!.loginWithOneClick,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Wrap(
|
|
||||||
children: [
|
|
||||||
for (final identityProvider in identityProviders)
|
|
||||||
SsoButton(
|
|
||||||
onPressed: () => controller
|
|
||||||
.ssoLoginAction(identityProvider.id!),
|
|
||||||
identityProvider: identityProvider,
|
|
||||||
),
|
|
||||||
].toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (controller.supportsLogin)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Hero(
|
|
||||||
tag: 'signinButton',
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.login_outlined),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
foregroundColor:
|
|
||||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
onPressed: controller.loading ? () {} : controller.login,
|
|
||||||
label: Text(L10n.of(context)!.login),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/pages/connect/connect_page.dart';
|
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
|
||||||
|
|
||||||
class SsoButton extends StatelessWidget {
|
|
||||||
final IdentityProvider identityProvider;
|
|
||||||
final void Function()? onPressed;
|
|
||||||
const SsoButton({
|
|
||||||
Key? key,
|
|
||||||
required this.identityProvider,
|
|
||||||
this.onPressed,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: onPressed,
|
|
||||||
borderRadius: BorderRadius.circular(7),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Material(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: identityProvider.icon == null
|
|
||||||
? const Icon(Icons.web_outlined)
|
|
||||||
: Image.network(
|
|
||||||
Uri.parse(identityProvider.icon!)
|
|
||||||
.getDownloadLink(
|
|
||||||
Matrix.of(context).getLoginClient(),
|
|
||||||
)
|
|
||||||
.toString(),
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
identityProvider.name ??
|
|
||||||
identityProvider.brand ??
|
|
||||||
L10n.of(context)!.singlesignon,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
|
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
|
import 'homeserver_bottom_sheet.dart';
|
||||||
import 'homeserver_picker.dart';
|
import 'homeserver_picker.dart';
|
||||||
|
|
||||||
class HomeserverAppBar extends StatelessWidget {
|
class HomeserverAppBar extends StatelessWidget {
|
||||||
@ -13,25 +16,57 @@ class HomeserverAppBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextField(
|
return TypeAheadField<HomeserverBenchmarkResult>(
|
||||||
focusNode: controller.homeserverFocusNode,
|
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||||
controller: controller.homeserverController,
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
onChanged: controller.onChanged,
|
elevation: Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
|
||||||
decoration: InputDecoration(
|
shadowColor: Theme.of(context).appBarTheme.shadowColor ?? Colors.black,
|
||||||
prefixIcon: Navigator.of(context).canPop()
|
constraints: const BoxConstraints(maxHeight: 256),
|
||||||
? IconButton(
|
),
|
||||||
onPressed: Navigator.of(context).pop,
|
itemBuilder: (context, homeserver) => ListTile(
|
||||||
icon: const Icon(Icons.arrow_back),
|
title: Text(homeserver.homeserver.baseUrl.toString()),
|
||||||
)
|
subtitle: Text(homeserver.homeserver.description ?? ''),
|
||||||
: null,
|
trailing: IconButton(
|
||||||
prefixText: '${L10n.of(context)!.homeserver}: ',
|
icon: const Icon(Icons.info_outlined),
|
||||||
hintText: L10n.of(context)!.enterYourHomeserver,
|
onPressed: () => showModalBottomSheet(
|
||||||
suffixIcon: const Icon(Icons.search),
|
context: context,
|
||||||
errorText: controller.error,
|
builder: (_) => HomeserverBottomSheet(
|
||||||
|
homeserver: homeserver,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
suggestionsCallback: (pattern) async {
|
||||||
|
final homeserverList =
|
||||||
|
await const JoinmatrixOrgParser().fetchHomeservers();
|
||||||
|
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
|
||||||
|
homeserverList,
|
||||||
|
timeout: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
return benchmark;
|
||||||
|
},
|
||||||
|
onSuggestionSelected: (suggestion) {
|
||||||
|
controller.homeserverController.text =
|
||||||
|
suggestion.homeserver.baseUrl.host;
|
||||||
|
controller.checkHomeserverAction();
|
||||||
|
},
|
||||||
|
textFieldConfiguration: TextFieldConfiguration(
|
||||||
|
controller: controller.homeserverController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: Navigator.of(context).canPop()
|
||||||
|
? IconButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
prefixText: '${L10n.of(context)!.homeserver}: ',
|
||||||
|
hintText: L10n.of(context)!.enterYourHomeserver,
|
||||||
|
suffixIcon: const Icon(Icons.search),
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
onSubmitted: controller.checkHomeserverAction,
|
||||||
|
autocorrect: false,
|
||||||
),
|
),
|
||||||
readOnly: !AppConfig.allowOtherHomeservers,
|
|
||||||
onSubmitted: (_) => controller.checkHomeserverAction(),
|
|
||||||
autocorrect: false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,16 +7,16 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
|
import 'package:universal_html/html.dart' as html;
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_bottom_sheet.dart';
|
|
||||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
|
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
|
||||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import '../../utils/localized_exception_extension.dart';
|
import '../../utils/localized_exception_extension.dart';
|
||||||
|
|
||||||
@ -35,14 +35,8 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
final TextEditingController homeserverController = TextEditingController(
|
final TextEditingController homeserverController = TextEditingController(
|
||||||
text: AppConfig.defaultHomeserver,
|
text: AppConfig.defaultHomeserver,
|
||||||
);
|
);
|
||||||
final FocusNode homeserverFocusNode = FocusNode();
|
|
||||||
String? error;
|
|
||||||
List<HomeserverBenchmarkResult>? benchmarkResults;
|
|
||||||
bool displayServerList = false;
|
|
||||||
|
|
||||||
bool get loadingHomeservers =>
|
String? error;
|
||||||
AppConfig.allowOtherHomeservers && benchmarkResults == null;
|
|
||||||
String searchTerm = '';
|
|
||||||
|
|
||||||
bool isTorBrowser = false;
|
bool isTorBrowser = false;
|
||||||
|
|
||||||
@ -65,98 +59,34 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
isTorBrowser = isTor;
|
isTorBrowser = isTor;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateFocus() {
|
String? _lastCheckedUrl;
|
||||||
if (benchmarkResults == null) _loadHomeserverList();
|
|
||||||
if (homeserverFocusNode.hasFocus) {
|
|
||||||
setState(() {
|
|
||||||
displayServerList = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showServerInfo(HomeserverBenchmarkResult server) =>
|
|
||||||
showAdaptiveBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => HomeserverBottomSheet(
|
|
||||||
homeserver: server,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
void onChanged(String text) => setState(() {
|
|
||||||
searchTerm = text;
|
|
||||||
});
|
|
||||||
|
|
||||||
List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults!
|
|
||||||
.where(
|
|
||||||
(element) =>
|
|
||||||
element.homeserver.baseUrl.host.contains(searchTerm) ||
|
|
||||||
(element.homeserver.description?.contains(searchTerm) ?? false),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
void _loadHomeserverList() async {
|
|
||||||
try {
|
|
||||||
final homeserverList =
|
|
||||||
await const JoinmatrixOrgParser().fetchHomeservers();
|
|
||||||
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
|
|
||||||
homeserverList,
|
|
||||||
timeout: const Duration(seconds: 10),
|
|
||||||
);
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
benchmarkResults = benchmark;
|
|
||||||
});
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().e('Homeserver benchmark failed', e, s);
|
|
||||||
benchmarkResults = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setServer(String server) => setState(() {
|
|
||||||
homeserverController.text = server;
|
|
||||||
searchTerm = '';
|
|
||||||
homeserverFocusNode.unfocus();
|
|
||||||
displayServerList = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Starts an analysis of the given homeserver. It uses the current domain and
|
/// Starts an analysis of the given homeserver. It uses the current domain and
|
||||||
/// makes sure that it is prefixed with https. Then it searches for the
|
/// makes sure that it is prefixed with https. Then it searches for the
|
||||||
/// well-known information and forwards to the login page depending on the
|
/// well-known information and forwards to the login page depending on the
|
||||||
/// login type.
|
/// login type.
|
||||||
Future<void> checkHomeserverAction() async {
|
Future<void> checkHomeserverAction([_]) async {
|
||||||
|
homeserverController.text =
|
||||||
|
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
|
||||||
|
if (homeserverController.text == _lastCheckedUrl) return;
|
||||||
|
_lastCheckedUrl = homeserverController.text;
|
||||||
setState(() {
|
setState(() {
|
||||||
homeserverFocusNode.unfocus();
|
error = _rawLoginTypes = loginHomeserverSummary = null;
|
||||||
error = null;
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
searchTerm = '';
|
|
||||||
displayServerList = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
homeserverController.text =
|
|
||||||
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
|
|
||||||
var homeserver = Uri.parse(homeserverController.text);
|
var homeserver = Uri.parse(homeserverController.text);
|
||||||
if (homeserver.scheme.isEmpty) {
|
if (homeserver.scheme.isEmpty) {
|
||||||
homeserver = Uri.https(homeserverController.text, '');
|
homeserver = Uri.https(homeserverController.text, '');
|
||||||
}
|
}
|
||||||
final matrix = Matrix.of(context);
|
final client = Matrix.of(context).getLoginClient();
|
||||||
matrix.loginHomeserverSummary =
|
loginHomeserverSummary = await client.checkHomeserver(homeserver);
|
||||||
await matrix.getLoginClient().checkHomeserver(homeserver);
|
if (supportsSso) {
|
||||||
final ssoSupported = matrix.loginHomeserverSummary!.loginFlows
|
_rawLoginTypes = await client.request(
|
||||||
.any((flow) => flow.type == 'm.login.sso');
|
RequestType.GET,
|
||||||
|
'/client/r0/login',
|
||||||
try {
|
);
|
||||||
await Matrix.of(context).getLoginClient().register();
|
|
||||||
matrix.loginRegistrationSupported = true;
|
|
||||||
} on MatrixException catch (e) {
|
|
||||||
matrix.loginRegistrationSupported = e.requireAdditionalAuthentication;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ssoSupported && matrix.loginRegistrationSupported == false) {
|
|
||||||
// Server does not support SSO or registration. We can skip to login page:
|
|
||||||
VRouter.of(context).to('login');
|
|
||||||
} else {
|
|
||||||
VRouter.of(context).to('connect');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => error = (e).toLocalizedString(context));
|
setState(() => error = (e).toLocalizedString(context));
|
||||||
@ -167,17 +97,71 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
HomeserverSummary? loginHomeserverSummary;
|
||||||
void dispose() {
|
|
||||||
homeserverFocusNode.removeListener(_updateFocus);
|
bool _supportsFlow(String flowType) =>
|
||||||
super.dispose();
|
loginHomeserverSummary?.loginFlows.any((flow) => flow.type == flowType) ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
bool get supportsSso => _supportsFlow('m.login.sso');
|
||||||
|
|
||||||
|
bool isDefaultPlatform =
|
||||||
|
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS);
|
||||||
|
|
||||||
|
bool get supportsPasswordLogin => _supportsFlow('m.login.password');
|
||||||
|
|
||||||
|
Map<String, dynamic>? _rawLoginTypes;
|
||||||
|
|
||||||
|
void ssoLoginAction(String id) async {
|
||||||
|
final redirectUrl = kIsWeb
|
||||||
|
? '${html.window.origin!}/web/auth.html'
|
||||||
|
: isDefaultPlatform
|
||||||
|
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
|
||||||
|
: 'http://localhost:3001//login';
|
||||||
|
final url =
|
||||||
|
'${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
|
||||||
|
final urlScheme = isDefaultPlatform
|
||||||
|
? Uri.parse(redirectUrl).scheme
|
||||||
|
: "http://localhost:3001";
|
||||||
|
final result = await FlutterWebAuth2.authenticate(
|
||||||
|
url: url,
|
||||||
|
callbackUrlScheme: urlScheme,
|
||||||
|
);
|
||||||
|
final token = Uri.parse(result).queryParameters['loginToken'];
|
||||||
|
if (token?.isEmpty ?? false) return;
|
||||||
|
|
||||||
|
await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => Matrix.of(context).getLoginClient().login(
|
||||||
|
LoginType.mLoginToken,
|
||||||
|
token: token,
|
||||||
|
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<IdentityProvider>? get identityProviders {
|
||||||
|
final loginTypes = _rawLoginTypes;
|
||||||
|
if (loginTypes == null) return null;
|
||||||
|
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
|
||||||
|
(flow) => flow['type'] == AuthenticationTypes.sso,
|
||||||
|
)['identity_providers'];
|
||||||
|
final list = (rawProviders as List)
|
||||||
|
.map((json) => IdentityProvider.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
if (PlatformInfos.isCupertinoStyle) {
|
||||||
|
list.sort((a, b) => a.brand == 'apple' ? -1 : 1);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
void login() => VRouter.of(context).to('login');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
homeserverFocusNode.addListener(_updateFocus);
|
|
||||||
_checkTorBrowser();
|
_checkTorBrowser();
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(checkHomeserverAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -204,3 +188,20 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IdentityProvider {
|
||||||
|
final String? id;
|
||||||
|
final String? name;
|
||||||
|
final String? icon;
|
||||||
|
final String? brand;
|
||||||
|
|
||||||
|
IdentityProvider({this.id, this.name, this.icon, this.brand});
|
||||||
|
|
||||||
|
factory IdentityProvider.fromJson(Map<String, dynamic> json) =>
|
||||||
|
IdentityProvider(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
icon: json['icon'],
|
||||||
|
brand: json['brand'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||||
import '../../config/themes.dart';
|
import '../../config/themes.dart';
|
||||||
|
import '../../widgets/mxc_image.dart';
|
||||||
import 'homeserver_app_bar.dart';
|
import 'homeserver_app_bar.dart';
|
||||||
import 'homeserver_picker.dart';
|
import 'homeserver_picker.dart';
|
||||||
|
|
||||||
@ -16,15 +16,19 @@ class HomeserverPickerView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final benchmarkResults = controller.benchmarkResults;
|
final identityProviders = controller.identityProviders;
|
||||||
|
final errorText = controller.error;
|
||||||
return LoginScaffold(
|
return LoginScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
titleSpacing: 12,
|
||||||
|
title: Padding(
|
||||||
|
padding: const EdgeInsets.all(0.0),
|
||||||
|
child: HomeserverAppBar(controller: controller),
|
||||||
|
),
|
||||||
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: HomeserverAppBar(controller: controller),
|
|
||||||
),
|
|
||||||
// display a prominent banner to import session for TOR browser
|
// display a prominent banner to import session for TOR browser
|
||||||
// users. This feature is just some UX sugar as TOR users are
|
// users. This feature is just some UX sugar as TOR users are
|
||||||
// usually forced to logout as TOR browser is non-persistent
|
// usually forced to logout as TOR browser is non-persistent
|
||||||
@ -49,108 +53,119 @@ class HomeserverPickerView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: controller.displayServerList
|
child: controller.isLoading
|
||||||
? ListView(
|
? const Center(child: CircularProgressIndicator.adaptive())
|
||||||
|
: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (controller.displayServerList)
|
Image.asset(
|
||||||
Padding(
|
'assets/info-logo.png',
|
||||||
padding: const EdgeInsets.all(12.0),
|
height: 96,
|
||||||
child: Material(
|
),
|
||||||
borderRadius:
|
Padding(
|
||||||
BorderRadius.circular(AppConfig.borderRadius),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
color: Theme.of(context)
|
child: Row(
|
||||||
.colorScheme
|
children: [
|
||||||
.onInverseSurface,
|
Expanded(
|
||||||
clipBehavior: Clip.hardEdge,
|
child: Divider(
|
||||||
child: benchmarkResults == null
|
thickness: 1,
|
||||||
? const Center(
|
height: 1,
|
||||||
child: Padding(
|
color: Theme.of(context).dividerColor,
|
||||||
padding: EdgeInsets.all(12.0),
|
),
|
||||||
child: CircularProgressIndicator
|
),
|
||||||
.adaptive(),
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.all(12.0),
|
||||||
)
|
child: Text(
|
||||||
: Column(
|
L10n.of(context)!.continueWith,
|
||||||
children: controller.filteredHomeservers
|
style: const TextStyle(fontSize: 12),
|
||||||
.map(
|
),
|
||||||
(server) => ListTile(
|
),
|
||||||
trailing: IconButton(
|
Expanded(
|
||||||
icon: const Icon(
|
child: Divider(
|
||||||
Icons.info_outlined,
|
thickness: 1,
|
||||||
color: Colors.black,
|
height: 1,
|
||||||
),
|
color: Theme.of(context).dividerColor,
|
||||||
onPressed: () => controller
|
),
|
||||||
.showServerInfo(server),
|
),
|
||||||
),
|
],
|
||||||
onTap: () => controller.setServer(
|
),
|
||||||
server.homeserver.baseUrl.host,
|
),
|
||||||
),
|
if (errorText != null) ...[
|
||||||
title: Text(
|
const Center(
|
||||||
server.homeserver.baseUrl.host,
|
child: Icon(
|
||||||
style: const TextStyle(
|
Icons.error_outline,
|
||||||
color: Colors.black,
|
size: 48,
|
||||||
),
|
color: Colors.orange,
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
server.homeserver.description ??
|
|
||||||
'',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: 12),
|
||||||
)
|
Center(
|
||||||
: Container(
|
child: Text(
|
||||||
alignment: Alignment.topCenter,
|
errorText,
|
||||||
child: Image.asset(
|
textAlign: TextAlign.center,
|
||||||
'assets/banner_transparent.png',
|
style: TextStyle(
|
||||||
filterQuality: FilterQuality.medium,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SafeArea(
|
),
|
||||||
child: Container(
|
Center(
|
||||||
padding: const EdgeInsets.all(12),
|
child: Text(
|
||||||
width: double.infinity,
|
L10n.of(context)!
|
||||||
child: Column(
|
.pleaseTryAgainLaterOrChooseDifferentServer,
|
||||||
mainAxisSize: MainAxisSize.min,
|
textAlign: TextAlign.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
style: TextStyle(
|
||||||
children: [
|
color: Theme.of(context).colorScheme.error,
|
||||||
TextButton(
|
fontSize: 12,
|
||||||
onPressed: () => launchUrlString(AppConfig.privacyUrl),
|
),
|
||||||
child: Text(L10n.of(context)!.privacy),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
const SizedBox(height: 12),
|
||||||
onPressed: controller.restoreBackup,
|
],
|
||||||
child: Text(L10n.of(context)!.hydrate),
|
if (identityProviders != null) ...[
|
||||||
),
|
...identityProviders.map(
|
||||||
Hero(
|
(provider) => _LoginButton(
|
||||||
tag: 'loginButton',
|
icon: provider.icon == null
|
||||||
child: ElevatedButton.icon(
|
? const Icon(Icons.open_in_new_outlined)
|
||||||
style: ElevatedButton.styleFrom(
|
: Material(
|
||||||
backgroundColor:
|
color: Colors.white,
|
||||||
Theme.of(context).colorScheme.primary,
|
borderRadius: BorderRadius.circular(
|
||||||
foregroundColor:
|
AppConfig.borderRadius,
|
||||||
Theme.of(context).colorScheme.onPrimary,
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: MxcImage(
|
||||||
|
placeholder: (_) =>
|
||||||
|
const Icon(Icons.web_outlined),
|
||||||
|
uri: Uri.parse(provider.icon!),
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label: provider.name ??
|
||||||
|
provider.brand ??
|
||||||
|
L10n.of(context)!.singlesignon,
|
||||||
|
onPressed: () =>
|
||||||
|
controller.ssoLoginAction(provider.id!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (controller.supportsPasswordLogin)
|
||||||
|
_LoginButton(
|
||||||
|
onPressed: controller.login,
|
||||||
|
icon: const Icon(Icons.login_outlined),
|
||||||
|
label: L10n.of(context)!.signInWithPassword,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
onPressed: controller.restoreBackup,
|
||||||
|
child: Text(L10n.of(context)!.hydrate),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: controller.isLoading
|
],
|
||||||
? null
|
|
||||||
: controller.checkHomeserverAction,
|
|
||||||
icon: const Icon(Icons.start_outlined),
|
|
||||||
label: controller.isLoading
|
|
||||||
? const LinearProgressIndicator()
|
|
||||||
: Text(L10n.of(context)!.letsStart),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -158,3 +173,33 @@ class HomeserverPickerView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _LoginButton extends StatelessWidget {
|
||||||
|
final Widget icon;
|
||||||
|
final String label;
|
||||||
|
final void Function() onPressed;
|
||||||
|
|
||||||
|
const _LoginButton({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: icon,
|
||||||
|
label: Text(label),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -109,6 +109,10 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
Widget body;
|
Widget body;
|
||||||
final buttons = <Widget>[];
|
final buttons = <Widget>[];
|
||||||
switch (widget.request.state) {
|
switch (widget.request.state) {
|
||||||
|
case KeyVerificationState.showQRSuccess:
|
||||||
|
case KeyVerificationState.confirmQRScan:
|
||||||
|
case KeyVerificationState.askChoice:
|
||||||
|
throw 'Not implemented';
|
||||||
case KeyVerificationState.askSSSS:
|
case KeyVerificationState.askSSSS:
|
||||||
// prompt the user for their ssss passphrase / key
|
// prompt the user for their ssss passphrase / key
|
||||||
final textEditingController = TextEditingController();
|
final textEditingController = TextEditingController();
|
||||||
|
|||||||
@ -59,7 +59,7 @@ class NewPrivateChatView extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
QrImage(
|
QrImageView(
|
||||||
data:
|
data:
|
||||||
'https://matrix.to/#/${Matrix.of(context).client.userID}',
|
'https://matrix.to/#/${Matrix.of(context).client.userID}',
|
||||||
version: QrVersions.auto,
|
version: QrVersions.auto,
|
||||||
|
|||||||
@ -151,12 +151,11 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
|
|
||||||
bool isGloballyActive(Client? client) =>
|
bool isGloballyActive(Client? client) =>
|
||||||
room != null &&
|
room != null &&
|
||||||
client!.accountData['im.ponies.emote_rooms']?.content is Map &&
|
client!.accountData['im.ponies.emote_rooms']?.content
|
||||||
client.accountData['im.ponies.emote_rooms']!.content['rooms'] is Map &&
|
.tryGetMap<String, Object?>('rooms')
|
||||||
client.accountData['im.ponies.emote_rooms']!.content['rooms'][room!.id]
|
?.tryGetMap<String, Object?>(room!.id)
|
||||||
is Map &&
|
?.tryGetMap<String, Object?>(stateKey ?? '') !=
|
||||||
client.accountData['im.ponies.emote_rooms']!.content['rooms'][room!.id]
|
null;
|
||||||
[stateKey ?? ''] is Map;
|
|
||||||
|
|
||||||
bool get readonly =>
|
bool get readonly =>
|
||||||
room == null ? false : !(room!.canSendEvent('im.ponies.room_emotes'));
|
room == null ? false : !(room!.canSendEvent('im.ponies.room_emotes'));
|
||||||
|
|||||||
@ -40,16 +40,14 @@ class MultipleEmotesSettingsView extends StatelessWidget {
|
|||||||
itemCount: keys.length,
|
itemCount: keys.length,
|
||||||
itemBuilder: (BuildContext context, int i) {
|
itemBuilder: (BuildContext context, int i) {
|
||||||
final event = packs[keys[i]];
|
final event = packs[keys[i]];
|
||||||
String? packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
|
final eventPack =
|
||||||
if (event != null && event.content['pack'] is Map) {
|
event?.content.tryGetMap<String, Object?>('pack');
|
||||||
if (event.content['pack']['displayname'] is String) {
|
final packName = eventPack?.tryGet<String>('displayname') ??
|
||||||
packName = event.content['pack']['displayname'];
|
eventPack?.tryGet<String>('name') ??
|
||||||
} else if (event.content['pack']['name'] is String) {
|
(keys[i].isNotEmpty ? keys[i] : 'Default Pack');
|
||||||
packName = event.content['pack']['name'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(packName!),
|
title: Text(packName),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
VRouter.of(context).toSegments(
|
VRouter.of(context).toSegments(
|
||||||
['rooms', room.id, 'details', 'emotes', keys[i]],
|
['rooms', room.id, 'details', 'emotes', keys[i]],
|
||||||
|
|||||||
@ -1,129 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:vrouter/vrouter.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/pages/sign_up/signup_view.dart';
|
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
|
||||||
import '../../utils/localized_exception_extension.dart';
|
|
||||||
|
|
||||||
class SignupPage extends StatefulWidget {
|
|
||||||
const SignupPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
SignupPageController createState() => SignupPageController();
|
|
||||||
}
|
|
||||||
|
|
||||||
class SignupPageController extends State<SignupPage> {
|
|
||||||
final TextEditingController passwordController = TextEditingController();
|
|
||||||
final TextEditingController password2Controller = TextEditingController();
|
|
||||||
final TextEditingController emailController = TextEditingController();
|
|
||||||
String? error;
|
|
||||||
bool loading = false;
|
|
||||||
bool showPassword = false;
|
|
||||||
bool noEmailWarningConfirmed = false;
|
|
||||||
bool displaySecondPasswordField = false;
|
|
||||||
|
|
||||||
static const int minPassLength = 8;
|
|
||||||
|
|
||||||
void toggleShowPassword() => setState(() => showPassword = !showPassword);
|
|
||||||
|
|
||||||
String? get domain => VRouter.of(context).queryParameters['domain'];
|
|
||||||
|
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
void onPasswordType(String text) {
|
|
||||||
if (text.length >= minPassLength && !displaySecondPasswordField) {
|
|
||||||
setState(() {
|
|
||||||
displaySecondPasswordField = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? password1TextFieldValidator(String? value) {
|
|
||||||
if (value!.isEmpty) {
|
|
||||||
return L10n.of(context)!.chooseAStrongPassword;
|
|
||||||
}
|
|
||||||
if (value.length < minPassLength) {
|
|
||||||
return L10n.of(context)!
|
|
||||||
.pleaseChooseAtLeastChars(minPassLength.toString());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? password2TextFieldValidator(String? value) {
|
|
||||||
if (value!.isEmpty) {
|
|
||||||
return L10n.of(context)!.repeatPassword;
|
|
||||||
}
|
|
||||||
if (value != passwordController.text) {
|
|
||||||
return L10n.of(context)!.passwordsDoNotMatch;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? emailTextFieldValidator(String? value) {
|
|
||||||
if (value!.isEmpty && !noEmailWarningConfirmed) {
|
|
||||||
noEmailWarningConfirmed = true;
|
|
||||||
return L10n.of(context)!.noEmailWarning;
|
|
||||||
}
|
|
||||||
if (value.isNotEmpty && !value.contains('@')) {
|
|
||||||
return L10n.of(context)!.pleaseEnterValidEmail;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void signup([_]) async {
|
|
||||||
setState(() {
|
|
||||||
error = null;
|
|
||||||
});
|
|
||||||
if (!formKey.currentState!.validate()) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
loading = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
final client = Matrix.of(context).getLoginClient();
|
|
||||||
final email = emailController.text;
|
|
||||||
if (email.isNotEmpty) {
|
|
||||||
Matrix.of(context).currentClientSecret =
|
|
||||||
DateTime.now().millisecondsSinceEpoch.toString();
|
|
||||||
Matrix.of(context).currentThreepidCreds =
|
|
||||||
await client.requestTokenToRegisterEmail(
|
|
||||||
Matrix.of(context).currentClientSecret,
|
|
||||||
email,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final displayname = Matrix.of(context).loginUsername!;
|
|
||||||
final localPart = displayname.toLowerCase().replaceAll(' ', '_');
|
|
||||||
|
|
||||||
await client.uiaRequestBackground(
|
|
||||||
(auth) => client.register(
|
|
||||||
username: localPart,
|
|
||||||
password: passwordController.text,
|
|
||||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
|
||||||
auth: auth,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// Set displayname
|
|
||||||
if (displayname != localPart) {
|
|
||||||
await client.setDisplayName(
|
|
||||||
client.userID!,
|
|
||||||
displayname,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
error = (e).toLocalizedString(context);
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => loading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => SignupPageView(this);
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
|
||||||
import 'signup.dart';
|
|
||||||
|
|
||||||
class SignupPageView extends StatelessWidget {
|
|
||||||
final SignupPageController controller;
|
|
||||||
const SignupPageView(this.controller, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return LoginScaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: controller.loading ? null : const BackButton(),
|
|
||||||
automaticallyImplyLeading: !controller.loading,
|
|
||||||
title: Text(L10n.of(context)!.signUp),
|
|
||||||
),
|
|
||||||
body: Form(
|
|
||||||
key: controller.formKey,
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: TextFormField(
|
|
||||||
readOnly: controller.loading,
|
|
||||||
autocorrect: false,
|
|
||||||
onChanged: controller.onPasswordType,
|
|
||||||
autofillHints:
|
|
||||||
controller.loading ? null : [AutofillHints.newPassword],
|
|
||||||
controller: controller.passwordController,
|
|
||||||
obscureText: !controller.showPassword,
|
|
||||||
validator: controller.password1TextFieldValidator,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.vpn_key_outlined),
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
tooltip: L10n.of(context)!.showPassword,
|
|
||||||
icon: Icon(
|
|
||||||
controller.showPassword
|
|
||||||
? Icons.visibility_off_outlined
|
|
||||||
: Icons.visibility_outlined,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
onPressed: controller.toggleShowPassword,
|
|
||||||
),
|
|
||||||
errorStyle: const TextStyle(color: Colors.orange),
|
|
||||||
hintText: L10n.of(context)!.chooseAStrongPassword,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (controller.displaySecondPasswordField)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: TextFormField(
|
|
||||||
readOnly: controller.loading,
|
|
||||||
autocorrect: false,
|
|
||||||
autofillHints:
|
|
||||||
controller.loading ? null : [AutofillHints.newPassword],
|
|
||||||
controller: controller.password2Controller,
|
|
||||||
obscureText: !controller.showPassword,
|
|
||||||
validator: controller.password2TextFieldValidator,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.repeat_outlined),
|
|
||||||
hintText: L10n.of(context)!.repeatPassword,
|
|
||||||
errorStyle: const TextStyle(color: Colors.orange),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: TextFormField(
|
|
||||||
readOnly: controller.loading,
|
|
||||||
autocorrect: false,
|
|
||||||
controller: controller.emailController,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
autofillHints:
|
|
||||||
controller.loading ? null : [AutofillHints.username],
|
|
||||||
validator: controller.emailTextFieldValidator,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.mail_outlined),
|
|
||||||
hintText: L10n.of(context)!.enterAnEmailAddress,
|
|
||||||
errorText: controller.error,
|
|
||||||
errorMaxLines: 4,
|
|
||||||
errorStyle: TextStyle(
|
|
||||||
color: controller.emailController.text.isEmpty
|
|
||||||
? Colors.orangeAccent
|
|
||||||
: Colors.orange,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Hero(
|
|
||||||
tag: 'loginButton',
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.person_add_outlined),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
onPressed: controller.loading ? () {} : controller.signup,
|
|
||||||
label: controller.loading
|
|
||||||
? const LinearProgressIndicator()
|
|
||||||
: Text(L10n.of(context)!.signUp),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -182,8 +182,12 @@ class MatrixLocals extends MatrixLocalizations {
|
|||||||
String get noPermission => l10n.noKeyForThisMessage;
|
String get noPermission => l10n.noKeyForThisMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String redactedAnEvent(String senderName) {
|
String redactedAnEvent(Event redactedEvent) {
|
||||||
return l10n.redactedAnEvent(senderName);
|
return l10n.redactedAnEvent(
|
||||||
|
redactedEvent.redactedBecause?.senderFromMemoryOrFallback
|
||||||
|
.calcLocalizedBodyFallback(this) ??
|
||||||
|
l10n.user,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -192,8 +196,10 @@ class MatrixLocals extends MatrixLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String removedBy(String calcDisplayname) {
|
String removedBy(Event redactedEvent) {
|
||||||
return l10n.removedBy(calcDisplayname);
|
return l10n.removedBy(
|
||||||
|
redactedEvent.senderFromMemoryOrFallback.calcLocalizedBodyFallback(this),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -198,11 +198,33 @@ Future<void> _tryPushHelper(
|
|||||||
|
|
||||||
final roomName = event.room.getLocalizedDisplayname(MatrixLocals(l10n));
|
final roomName = event.room.getLocalizedDisplayname(MatrixLocals(l10n));
|
||||||
|
|
||||||
|
final notificationGroupId =
|
||||||
|
event.room.isDirectChat ? 'directChats' : 'groupChats';
|
||||||
|
final groupName = event.room.isDirectChat ? l10n.directChats : l10n.groups;
|
||||||
|
|
||||||
|
final messageRooms = AndroidNotificationChannelGroup(
|
||||||
|
notificationGroupId,
|
||||||
|
groupName,
|
||||||
|
);
|
||||||
|
final roomsChannel = AndroidNotificationChannel(
|
||||||
|
event.room.id,
|
||||||
|
roomName,
|
||||||
|
groupId: notificationGroupId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannelGroup(messageRooms);
|
||||||
|
await flutterLocalNotificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(roomsChannel);
|
||||||
|
|
||||||
final androidPlatformChannelSpecifics = AndroidNotificationDetails(
|
final androidPlatformChannelSpecifics = AndroidNotificationDetails(
|
||||||
event.room.id,
|
event.room.id,
|
||||||
roomName,
|
roomName,
|
||||||
channelDescription:
|
channelDescription: groupName,
|
||||||
event.room.isDirectChat ? l10n.directChats : l10n.groups,
|
|
||||||
number: notification.counts?.unread,
|
number: notification.counts?.unread,
|
||||||
category: AndroidNotificationCategory.message,
|
category: AndroidNotificationCategory.message,
|
||||||
styleInformation: messagingStyleInformation ??
|
styleInformation: messagingStyleInformation ??
|
||||||
@ -215,7 +237,7 @@ Future<void> _tryPushHelper(
|
|||||||
ticker: l10n.unreadChats(notification.counts?.unread ?? 1),
|
ticker: l10n.unreadChats(notification.counts?.unread ?? 1),
|
||||||
importance: Importance.max,
|
importance: Importance.max,
|
||||||
priority: Priority.max,
|
priority: Priority.max,
|
||||||
groupKey: event.room.id,
|
groupKey: notificationGroupId,
|
||||||
);
|
);
|
||||||
const iOSPlatformChannelSpecifics = DarwinNotificationDetails();
|
const iOSPlatformChannelSpecifics = DarwinNotificationDetails();
|
||||||
final platformChannelSpecifics = NotificationDetails(
|
final platformChannelSpecifics = NotificationDetails(
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
import 'package:collection/collection.dart' show IterableExtension;
|
import 'package:collection/collection.dart' show IterableExtension;
|
||||||
@ -37,14 +38,34 @@ class UrlLauncher {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final consent = await showOkCancelAlertDialog(
|
final consent = await showModalActionSheet<_LaunchUrlResponse>(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.openLinkInBrowser,
|
title: url,
|
||||||
message: url,
|
style: AdaptiveStyle.material,
|
||||||
okLabel: L10n.of(context)!.ok,
|
actions: [
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
SheetAction(
|
||||||
|
key: _LaunchUrlResponse.copy,
|
||||||
|
icon: Icons.copy_outlined,
|
||||||
|
label: L10n.of(context)!.copy,
|
||||||
|
),
|
||||||
|
SheetAction(
|
||||||
|
key: _LaunchUrlResponse.launch,
|
||||||
|
icon: Icons.launch_outlined,
|
||||||
|
label: L10n.of(context)!.openLinkInBrowser,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
if (consent != OkCancelResult.ok) return;
|
if (consent == _LaunchUrlResponse.copy) {
|
||||||
|
await Clipboard.setData(ClipboardData(text: uri.toString()));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(L10n.of(context)!.copiedToClipboard),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consent != _LaunchUrlResponse.launch) return;
|
||||||
|
|
||||||
if (!{'https', 'http'}.contains(uri.scheme)) {
|
if (!{'https', 'http'}.contains(uri.scheme)) {
|
||||||
// just launch non-https / non-http uris directly
|
// just launch non-https / non-http uris directly
|
||||||
@ -215,3 +236,8 @@ class UrlLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _LaunchUrlResponse {
|
||||||
|
launch,
|
||||||
|
copy,
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
|
|
||||||
class LoginScaffold extends StatelessWidget {
|
class LoginScaffold extends StatelessWidget {
|
||||||
final Widget body;
|
final Widget body;
|
||||||
@ -17,6 +21,7 @@ class LoginScaffold extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isMobileMode = !FluffyThemes.isColumnMode(context);
|
final isMobileMode = !FluffyThemes.isColumnMode(context);
|
||||||
final scaffold = Scaffold(
|
final scaffold = Scaffold(
|
||||||
|
key: const Key('LoginScaffold'),
|
||||||
backgroundColor: isMobileMode ? null : Colors.transparent,
|
backgroundColor: isMobileMode ? null : Colors.transparent,
|
||||||
appBar: appBar == null
|
appBar: appBar == null
|
||||||
? null
|
? null
|
||||||
@ -33,31 +38,93 @@ class LoginScaffold extends StatelessWidget {
|
|||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
body: body,
|
body: body,
|
||||||
|
bottomNavigationBar: isMobileMode
|
||||||
|
? Material(
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
|
child: const _PrivacyButtons(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
if (isMobileMode) return scaffold;
|
if (isMobileMode) return scaffold;
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
gradient: FluffyThemes.backgroundGradient(context, 156),
|
||||||
image: AssetImage('assets/login_wallpaper.png'),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Column(
|
||||||
child: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.all(16.0),
|
const SizedBox(height: 64),
|
||||||
child: Material(
|
Expanded(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.925),
|
child: Center(
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
child: Padding(
|
||||||
clipBehavior: Clip.hardEdge,
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
elevation: 10,
|
child: Material(
|
||||||
shadowColor: Colors.black,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
child: ConstrainedBox(
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
constraints: isMobileMode
|
clipBehavior: Clip.hardEdge,
|
||||||
? const BoxConstraints()
|
elevation:
|
||||||
: const BoxConstraints(maxWidth: 480, maxHeight: 640),
|
Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
|
||||||
child: scaffold,
|
shadowColor: Theme.of(context).appBarTheme.shadowColor,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: isMobileMode
|
||||||
|
? const BoxConstraints()
|
||||||
|
: const BoxConstraints(maxWidth: 960, maxHeight: 640),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/login_wallpaper.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
color: Theme.of(context).dividerTheme.color,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: scaffold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const _PrivacyButtons(mainAxisAlignment: MainAxisAlignment.end),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PrivacyButtons extends StatelessWidget {
|
||||||
|
final MainAxisAlignment mainAxisAlignment;
|
||||||
|
const _PrivacyButtons({required this.mainAxisAlignment});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 64,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: mainAxisAlignment,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => PlatformInfos.showDialog(context),
|
||||||
|
child: Text(L10n.of(context)!.about),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => launchUrlString(AppConfig.privacyUrl),
|
||||||
|
child: Text(L10n.of(context)!.privacy),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,7 +10,9 @@
|
|||||||
#include <desktop_lifecycle/desktop_lifecycle_plugin.h>
|
#include <desktop_lifecycle/desktop_lifecycle_plugin.h>
|
||||||
#include <dynamic_color/dynamic_color_plugin.h>
|
#include <dynamic_color/dynamic_color_plugin.h>
|
||||||
#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
|
#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <handy_window/handy_window_plugin.h>
|
#include <handy_window/handy_window_plugin.h>
|
||||||
#include <record_linux/record_linux_plugin.h>
|
#include <record_linux/record_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
@ -29,9 +31,15 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar =
|
g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin");
|
||||||
emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar);
|
emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||||
|
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) handy_window_registrar =
|
g_autoptr(FlPluginRegistrar) handy_window_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin");
|
||||||
handy_window_plugin_register_with_registrar(handy_window_registrar);
|
handy_window_plugin_register_with_registrar(handy_window_registrar);
|
||||||
|
|||||||
@ -7,7 +7,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
desktop_lifecycle
|
desktop_lifecycle
|
||||||
dynamic_color
|
dynamic_color
|
||||||
emoji_picker_flutter
|
emoji_picker_flutter
|
||||||
|
file_selector_linux
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
|
flutter_webrtc
|
||||||
handy_window
|
handy_window
|
||||||
record_linux
|
record_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import desktop_lifecycle
|
|||||||
import device_info_plus
|
import device_info_plus
|
||||||
import dynamic_color
|
import dynamic_color
|
||||||
import emoji_picker_flutter
|
import emoji_picker_flutter
|
||||||
|
import file_selector_macos
|
||||||
import flutter_app_badger
|
import flutter_app_badger
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
@ -19,15 +20,18 @@ import flutter_web_auth_2
|
|||||||
import flutter_webrtc
|
import flutter_webrtc
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import just_audio
|
import just_audio
|
||||||
|
import macos_ui
|
||||||
|
import macos_window_utils
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import record_macos
|
import record_macos
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_macos
|
import shared_preferences_foundation
|
||||||
import sqflite
|
import sqflite
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import video_compress
|
import video_compress
|
||||||
import wakelock_macos
|
import wakelock_macos
|
||||||
|
import wakelock_plus
|
||||||
import window_to_front
|
import window_to_front
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
@ -38,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||||
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
|
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin"))
|
FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
@ -45,6 +50,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||||
|
MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin"))
|
||||||
|
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
|
||||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
RecordMacosPlugin.register(with: registry.registrar(forPlugin: "RecordMacosPlugin"))
|
RecordMacosPlugin.register(with: registry.registrar(forPlugin: "RecordMacosPlugin"))
|
||||||
@ -54,5 +61,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
|
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
|
||||||
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
|
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
|
||||||
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
|
WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
617
pubspec.lock
617
pubspec.lock
File diff suppressed because it is too large
Load Diff
36
pubspec.yaml
36
pubspec.yaml
@ -7,19 +7,19 @@ environment:
|
|||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_dialog: ^1.9.0-no-macos.2
|
adaptive_dialog: ^1.9.0-x-macos-beta.1
|
||||||
animations: ^2.0.7
|
animations: ^2.0.7
|
||||||
badges: ^2.0.3
|
badges: ^2.0.3
|
||||||
blurhash_dart: ^1.1.0
|
blurhash_dart: ^1.1.0
|
||||||
callkeep: ^0.3.2
|
callkeep: ^0.3.2
|
||||||
chewie: ^1.3.6
|
chewie: ^1.3.6
|
||||||
collection: ^1.16.0
|
collection: ^1.16.0
|
||||||
connectivity_plus: ^3.0.2
|
connectivity_plus: ^4.0.1
|
||||||
cupertino_icons: any
|
cupertino_icons: any
|
||||||
desktop_drop: ^0.4.0
|
desktop_drop: ^0.4.0
|
||||||
desktop_lifecycle: ^0.1.0
|
desktop_lifecycle: ^0.1.0
|
||||||
desktop_notifications: ^0.6.3
|
desktop_notifications: ^0.6.3
|
||||||
device_info_plus: ^8.0.0
|
device_info_plus: ^9.0.2
|
||||||
dynamic_color: ^1.6.0
|
dynamic_color: ^1.6.0
|
||||||
emoji_picker_flutter: ^1.5.1
|
emoji_picker_flutter: ^1.5.1
|
||||||
emoji_proposal: ^0.0.1
|
emoji_proposal: ^0.0.1
|
||||||
@ -32,37 +32,37 @@ dependencies:
|
|||||||
flutter_app_lock: ^3.0.0
|
flutter_app_lock: ^3.0.0
|
||||||
flutter_blurhash: ^0.7.0
|
flutter_blurhash: ^0.7.0
|
||||||
flutter_cache_manager: ^3.3.0
|
flutter_cache_manager: ^3.3.0
|
||||||
flutter_foreground_task: ^3.10.0
|
flutter_foreground_task: ^6.0.0+1
|
||||||
flutter_highlighter: ^0.1.1
|
flutter_highlighter: ^0.1.1
|
||||||
flutter_html: ^3.0.0-beta.2
|
flutter_html: ^3.0.0-beta.2
|
||||||
flutter_html_table: ^3.0.0-beta.2
|
flutter_html_table: ^3.0.0-beta.2
|
||||||
flutter_linkify: ^6.0.0
|
flutter_linkify: ^6.0.0
|
||||||
flutter_local_notifications: ^12.0.2
|
flutter_local_notifications: ^15.1.0+1
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_map: ^3.1.0
|
flutter_map: ^4.0.0
|
||||||
flutter_math_fork: ^0.7.1
|
flutter_math_fork: ^0.7.1
|
||||||
flutter_olm: ^1.2.0
|
flutter_olm: ^1.2.0
|
||||||
flutter_openssl_crypto: ^0.1.0
|
flutter_openssl_crypto: ^0.1.0
|
||||||
flutter_ringtone_player: ^3.1.1
|
flutter_ringtone_player: ^3.1.1
|
||||||
flutter_secure_storage: ^7.0.1
|
flutter_secure_storage: ^8.0.0
|
||||||
flutter_typeahead: ^4.3.2
|
flutter_typeahead: ^4.3.2
|
||||||
flutter_web_auth_2: ^2.1.1
|
flutter_web_auth_2: ^2.1.1
|
||||||
flutter_webrtc: ^0.9.30+hotfix.2
|
flutter_webrtc: ^0.9.35
|
||||||
future_loading_dialog: ^0.2.3
|
future_loading_dialog: ^0.2.3
|
||||||
geolocator: ^7.6.2
|
geolocator: ^7.6.2
|
||||||
handy_window: ^0.1.9
|
handy_window: ^0.3.1
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
http: ^0.13.4
|
http: ^0.13.4
|
||||||
image_picker: ^0.8.4+8
|
image_picker: ^1.0.0
|
||||||
intl: any
|
intl: any
|
||||||
just_audio: ^0.9.30
|
just_audio: ^0.9.30
|
||||||
just_audio_mpv: ^0.1.6
|
just_audio_mpv: ^0.1.6
|
||||||
keyboard_shortcuts: ^0.1.4
|
keyboard_shortcuts: ^0.1.4
|
||||||
latlong2: ^0.8.1
|
latlong2: ^0.8.1
|
||||||
linkify: ^5.0.0
|
linkify: ^5.0.0
|
||||||
matrix: ^0.20.5
|
matrix: ^0.22.0
|
||||||
matrix_homeserver_recommendations: ^0.3.0
|
matrix_homeserver_recommendations: ^0.3.0
|
||||||
native_imaging: ^0.1.0
|
native_imaging: ^0.1.0
|
||||||
package_info_plus: ^4.0.0
|
package_info_plus: ^4.0.0
|
||||||
@ -77,12 +77,12 @@ dependencies:
|
|||||||
record: ^4.4.4
|
record: ^4.4.4
|
||||||
scroll_to_index: ^3.0.1
|
scroll_to_index: ^3.0.1
|
||||||
share_plus: ^7.0.0
|
share_plus: ^7.0.0
|
||||||
shared_preferences: 2.0.15 # Pinned because https://github.com/flutter/flutter/issues/118401
|
shared_preferences: ^2.2.0 # Pinned because https://github.com/flutter/flutter/issues/118401
|
||||||
slugify: ^2.0.0
|
slugify: ^2.0.0
|
||||||
swipe_to_action: ^0.2.0
|
swipe_to_action: ^0.2.0
|
||||||
tor_detector_web: ^1.1.0
|
tor_detector_web: ^1.1.0
|
||||||
uni_links: ^0.5.1
|
uni_links: ^0.5.1
|
||||||
unifiedpush: ^4.0.3
|
unifiedpush: ^5.0.0
|
||||||
universal_html: ^2.0.8
|
universal_html: ^2.0.8
|
||||||
url_launcher: ^6.0.20
|
url_launcher: ^6.0.20
|
||||||
vibration: ^1.7.4-nullsafety.0
|
vibration: ^1.7.4-nullsafety.0
|
||||||
@ -93,7 +93,7 @@ dependencies:
|
|||||||
webrtc_interface: ^1.0.13
|
webrtc_interface: ^1.0.13
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
dart_code_metrics: ^4.10.1
|
dart_code_metrics: ^5.7.5
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^2.0.1
|
||||||
flutter_native_splash: ^2.0.3+1
|
flutter_native_splash: ^2.0.3+1
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -153,8 +153,6 @@ dependency_overrides:
|
|||||||
git:
|
git:
|
||||||
url: https://gitlab.com/TheOneWithTheBraid/flutter_secure_storage_windows.git
|
url: https://gitlab.com/TheOneWithTheBraid/flutter_secure_storage_windows.git
|
||||||
ref: main
|
ref: main
|
||||||
flutter_webrtc:
|
|
||||||
git: https://github.com/krille-chan/flutter-webrtc.git
|
|
||||||
geolocator_android:
|
geolocator_android:
|
||||||
hosted:
|
hosted:
|
||||||
name: geolocator_android
|
name: geolocator_android
|
||||||
@ -169,6 +167,6 @@ dependency_overrides:
|
|||||||
# https://github.com/creativecreatorormaybenot/wakelock/pull/203
|
# https://github.com/creativecreatorormaybenot/wakelock/pull/203
|
||||||
wakelock_windows:
|
wakelock_windows:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/timsneath/wakelock.git
|
url: https://github.com/chandrabezzo/wakelock.git
|
||||||
ref: 2a9bca63a540771f241d688562351482b2cf234c
|
ref: main
|
||||||
path: wakelock_windows
|
path: wakelock_windows/
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
#include <desktop_lifecycle/desktop_lifecycle_plugin.h>
|
#include <desktop_lifecycle/desktop_lifecycle_plugin.h>
|
||||||
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||||
#include <emoji_picker_flutter/emoji_picker_flutter_plugin_c_api.h>
|
#include <emoji_picker_flutter/emoji_picker_flutter_plugin_c_api.h>
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <record_windows/record_windows_plugin_c_api.h>
|
#include <record_windows/record_windows_plugin_c_api.h>
|
||||||
@ -29,6 +30,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||||
EmojiPickerFlutterPluginCApiRegisterWithRegistrar(
|
EmojiPickerFlutterPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi"));
|
registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi"));
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
|||||||
@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
desktop_lifecycle
|
desktop_lifecycle
|
||||||
dynamic_color
|
dynamic_color
|
||||||
emoji_picker_flutter
|
emoji_picker_flutter
|
||||||
|
file_selector_windows
|
||||||
flutter_webrtc
|
flutter_webrtc
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
record_windows
|
record_windows
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user