feat(app): one Profile for every app, the person's own tab first, then each app's (ADR 0014)
The public profile was already one per person for every service of an instance (the register, ADR 0016); its app side was Learn's alone. It moves into halkyon_app: the model, the repository on the module's endpoints, the form, the avatar drawn on the device and the sheet with someone's profile. They take the Public prefix (PublicProfileRepository, BasePublicProfileGateway), as Train's own ProfileRepository holds the body's figures. AppDependencies gains profiles and files, the picker the avatar is chosen with, which moves out of Learn too. HalkyonProfile is every app's Profile screen: the person's own tab first, the same in every app (the account and the ways they sign in, the app's own cards, then the public profile), then the app's own tabs, folding into the title. Learn adds its inbox and Statistics. Train adds Progress, Days and, where nothing records, Plans, and so opens on the person's tab, no longer on Progress. Todo, which has no Profile tab, opens it from a row at the top of Browse, where Todoist keeps the account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
a0bb75f1ad
commit
b7b0b937fe
41 changed files with 1668 additions and 1293 deletions
File diff suppressed because one or more lines are too long
|
|
@ -447,8 +447,6 @@
|
|||
"exploreTitle": "Entdecken",
|
||||
"exploreEmpty": "Lernsets, die andere teilen, erscheinen hier.",
|
||||
|
||||
"profileTitle": "Profil",
|
||||
"profileTabProfile": "Profil",
|
||||
"profileTabStatistics": "Statistik",
|
||||
"statisticsDays": "Tage",
|
||||
"statisticsWeeks": "Wochen",
|
||||
|
|
@ -467,75 +465,9 @@
|
|||
"statisticsWeekOf": "Woche ab {date}",
|
||||
"@statisticsWeekOf": {"placeholders": {"date": {"type": "String"}}},
|
||||
"statisticsNothingYet": "Noch nichts gelernt. Deine Lernzeit und Bewertungen erscheinen hier, sobald du Karteikarten lernst.",
|
||||
"profileSettingsTooltip": "Einstellungen",
|
||||
"profileIdentified": "Dein Fortschritt wird in deinem Konto gespeichert.",
|
||||
"profileSignInWays": "Anmeldewege",
|
||||
"profileWayEmail": "E-Mail-Adresse",
|
||||
"profileWayLinked": "Meldet dich an",
|
||||
"publicProfileTitle": "Öffentliches Profil",
|
||||
"profileSheetTitle": "Profil",
|
||||
"profileSheetNotFound": "Niemand auf dieser Instanz hat diesen Handle.",
|
||||
"profileSheetLinkVerified": "Verlinkt zurück auf dieses Profil",
|
||||
"profileSheetUnreachable": "Das Profil konnte nicht geladen werden. Prüfe deine Verbindung.",
|
||||
"publicProfileIntro": "Wie andere dich sehen, sobald du veröffentlichst: das Handle, unter dem sie dich finden, dein Name, ein paar Worte und Links. Es speichert sich beim Schreiben, sobald du dein Handle gewählt hast.",
|
||||
"publicProfileDisplayName": "Anzeigename",
|
||||
"publicProfileDisplayNameHint": "Der Name neben deinem Handle",
|
||||
"publicProfileHandle": "Handle",
|
||||
"publicProfileHandleHint": "Buchstaben, Ziffern und Unterstriche, bis zu 30",
|
||||
"publicProfileAddress": "Andere finden dich als @{address}",
|
||||
"@publicProfileAddress": {"placeholders": {"address": {"type": "String"}}},
|
||||
"publicProfileAddressPrivate": "Privat, bis du dein Profil ausfüllst oder etwas veröffentlichst; dann finden dich andere als @{address}.",
|
||||
"@publicProfileAddressPrivate": {"placeholders": {"address": {"type": "String"}}},
|
||||
"publicProfileHandleFixed": "Fest, seit du veröffentlicht hast: Andere verweisen darauf.",
|
||||
"publicProfileBio": "Über dich",
|
||||
"publicProfileLinks": "Links",
|
||||
"publicProfileLinksHint": "Deine Website oder andere Konten, bis zu 4.",
|
||||
"publicProfileLinkHint": "https://…",
|
||||
"publicProfileAddLink": "Link hinzufügen",
|
||||
"publicProfilePage": "Deine Profilseite: {url}",
|
||||
"publicProfilePageCopy": "Adresse kopieren",
|
||||
"publicProfilePageCopied": "Adresse kopiert.",
|
||||
"publicProfileVerifyHint": "Ein Link bekommt ein Häkchen, sobald seine Seite mit rel=\"me\" auf deine Profilseite zurückverlinkt, wie Mastodon Links prüft.",
|
||||
"publicProfileCheckLinks": "Links prüfen",
|
||||
"publicProfileLinkVerified": "Verlinkt zurück auf dein Profil",
|
||||
"publicProfileLinksChecked": "{count, plural, =1{Geprüft: {verified} von 1 Link verlinkt zurück auf dein Profil.} other{Geprüft: {verified} von {count} Links verlinken zurück auf dein Profil.}}",
|
||||
"publicProfileRemoveLink": "Link entfernen",
|
||||
"publicProfileSaving": "Wird gespeichert …",
|
||||
"publicProfileSavedNow": "Gespeichert",
|
||||
"publicProfileHandleChoose": "Wähle ein Handle",
|
||||
"publicProfileHandleFirst": "Wähle zuerst dein Handle: Damit wird dein Profil angelegt, und ab dann speichert es sich beim Schreiben.",
|
||||
"publicProfileHandleChooseTitle": "Wähle dein Handle",
|
||||
"publicProfileHandleChangeTitle": "Ändere dein Handle",
|
||||
"publicProfileHandleDialogText": "Andere finden dich unter deinem Handle. Du kannst es ändern, bis du zum ersten Mal ein Lernset veröffentlichst.",
|
||||
"publicProfileHandleChecking": "Wird geprüft …",
|
||||
"publicProfileHandleFree": "@{handle} ist frei",
|
||||
"@publicProfileHandleFree": {"placeholders": {"handle": {"type": "String"}}},
|
||||
"publicProfileHandleYours": "Das ist dein Handle",
|
||||
"publicProfileHandleConfirmChoose": "Wählen",
|
||||
"publicProfileHandleConfirmChange": "Ändern",
|
||||
"publicProfileRemove": "Profil entfernen",
|
||||
"publicProfileRemoveTitle": "Dein öffentliches Profil entfernen?",
|
||||
"publicProfileRemoveMessage": "Anzeigename, Text über dich, Links und Bild werden entfernt. Dein Handle bleibt deins und privat, bis du dein Profil wieder ausfüllst oder etwas veröffentlichst.",
|
||||
"publicProfileRemoved": "Profil entfernt.",
|
||||
"publicProfileFailed": "Die Instanz ist nicht erreichbar. Versuche es erneut.",
|
||||
"publicProfileUnreachable": "Dein öffentliches Profil ist gerade nicht erreichbar.",
|
||||
"publicProfileRetry": "Erneut versuchen",
|
||||
"profileProblemHandleInvalid": "Ein Handle besteht aus 1 bis 30 Buchstaben (a bis z), Ziffern oder Unterstrichen.",
|
||||
"profileProblemHandleReserved": "Dieses Handle ist der Instanz vorbehalten.",
|
||||
"profileProblemHandleTaken": "Dieses Handle hat schon jemand anderes.",
|
||||
"profileProblemPublished": "Du hast veröffentlicht: Dein Handle kann sich nicht mehr ändern und dein Profil nicht mehr gehen.",
|
||||
"profileProblemDisplayNameTooLong": "Höchstens 30 Zeichen.",
|
||||
"profileProblemBioTooLong": "Höchstens 500 Zeichen.",
|
||||
"profileProblemTooManyLinks": "Höchstens 4 Links.",
|
||||
"profileProblemLinkInvalid": "Links beginnen mit https:// oder http://.",
|
||||
"profileProblemAvatarInvalid": "Dieses Bild lässt sich nicht verwenden: Wähle ein PNG-, JPEG-, WebP- oder GIF-Bild.",
|
||||
"profileProblemTooManyLinkChecks": "Die Links wurden zu oft geprüft. Versuche es in einer Stunde noch einmal.",
|
||||
"profileProblemTooManyHandleChecks": "Es wurden zu viele Handles ausprobiert. Versuche es in einer Stunde noch einmal.",
|
||||
"publicProfilePicture": "Bild",
|
||||
"publicProfileChoosePicture": "Bild wählen",
|
||||
"publicProfileRemovePicture": "Bild entfernen",
|
||||
"publicProfilePictureType": "Bilder",
|
||||
"publicProfilePictureLater": "Wähle dein Handle, um ein Bild hinzuzufügen.",
|
||||
|
||||
"settingsTitle": "Einstellungen",
|
||||
"settingsAccount": "Konto",
|
||||
|
|
|
|||
|
|
@ -504,8 +504,6 @@
|
|||
"exploreTitle": "Explore",
|
||||
"exploreEmpty": "Studysets that others share will appear here.",
|
||||
|
||||
"profileTitle": "Profile",
|
||||
"profileTabProfile": "Profile",
|
||||
"profileTabStatistics": "Statistics",
|
||||
"statisticsDays": "Days",
|
||||
"statisticsWeeks": "Weeks",
|
||||
|
|
@ -532,81 +530,9 @@
|
|||
}
|
||||
},
|
||||
"statisticsNothingYet": "Nothing studied yet. Your study time and ratings show here once you study flashcards.",
|
||||
"profileSettingsTooltip": "Settings",
|
||||
"profileIdentified": "Your progress is kept with your account.",
|
||||
"profileSignInWays": "Ways to sign in",
|
||||
"profileWayEmail": "Email address",
|
||||
"profileWayLinked": "Signs you in",
|
||||
"publicProfileTitle": "Public profile",
|
||||
"profileSheetTitle": "Profile",
|
||||
"profileSheetNotFound": "Nobody on this instance has this handle.",
|
||||
"profileSheetLinkVerified": "Links back to this profile",
|
||||
"profileSheetUnreachable": "The profile couldn’t be loaded. Check your connection.",
|
||||
"publicProfileIntro": "How others see you once you publish: the handle they find you by, your name, a few words and links. It saves as you write, once you have chosen your handle.",
|
||||
"publicProfileDisplayName": "Display name",
|
||||
"publicProfileDisplayNameHint": "The name shown beside your handle",
|
||||
"publicProfileHandle": "Handle",
|
||||
"publicProfileHandleHint": "Letters, digits and underscores, up to 30",
|
||||
"publicProfileAddress": "Others find you as @{address}",
|
||||
"@publicProfileAddress": {"placeholders": {"address": {"type": "String"}}},
|
||||
"publicProfileAddressPrivate": "Private until you fill in your profile or publish something; then others find you as @{address}.",
|
||||
"@publicProfileAddressPrivate": {"placeholders": {"address": {"type": "String"}}},
|
||||
"publicProfileHandleFixed": "Fixed since you published: others refer to it.",
|
||||
"publicProfileBio": "Bio",
|
||||
"publicProfileLinks": "Links",
|
||||
"publicProfileLinksHint": "Your site or other accounts, up to 4.",
|
||||
"publicProfileLinkHint": "https://…",
|
||||
"publicProfileAddLink": "Add link",
|
||||
"publicProfilePage": "Your profile page: {url}",
|
||||
"@publicProfilePage": {"placeholders": {"url": {"type": "String"}}},
|
||||
"publicProfilePageCopy": "Copy the address",
|
||||
"publicProfilePageCopied": "Address copied.",
|
||||
"publicProfileVerifyHint": "A link gets a checkmark once its page links back to your profile page with rel=\"me\", as Mastodon checks links.",
|
||||
"publicProfileCheckLinks": "Check links",
|
||||
"publicProfileLinkVerified": "Links back to your profile",
|
||||
"publicProfileLinksChecked": "{count, plural, =1{Checked: {verified} of 1 link links back to your profile.} other{Checked: {verified} of {count} links link back to your profile.}}",
|
||||
"@publicProfileLinksChecked": {"placeholders": {"count": {"type": "int"}, "verified": {"type": "int"}}},
|
||||
"publicProfileRemoveLink": "Remove link",
|
||||
"publicProfileSaving": "Saving…",
|
||||
"publicProfileSavedNow": "Saved",
|
||||
"publicProfileHandleChoose": "Choose a handle",
|
||||
"publicProfileHandleFirst": "Choose your handle first: your profile is made with it, and saves as you write from then on.",
|
||||
"publicProfileHandleChooseTitle": "Choose your handle",
|
||||
"publicProfileHandleChangeTitle": "Change your handle",
|
||||
"publicProfileHandleDialogText": "Others find you by your handle. You can change it until you first publish a studyset.",
|
||||
"publicProfileHandleChecking": "Checking…",
|
||||
"publicProfileHandleFree": "@{handle} is free",
|
||||
"@publicProfileHandleFree": {
|
||||
"placeholders": {
|
||||
"handle": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"publicProfileHandleYours": "This is your handle",
|
||||
"publicProfileHandleConfirmChoose": "Choose",
|
||||
"publicProfileHandleConfirmChange": "Change",
|
||||
"publicProfileRemove": "Remove profile",
|
||||
"publicProfileRemoveTitle": "Remove your public profile?",
|
||||
"publicProfileRemoveMessage": "Your display name, bio, links and picture are removed. Your handle stays yours and private until you fill in your profile again or publish something.",
|
||||
"publicProfileRemoved": "Profile removed.",
|
||||
"publicProfileFailed": "Couldn't reach the instance. Try again.",
|
||||
"publicProfileUnreachable": "Your public profile can't be reached right now.",
|
||||
"publicProfileRetry": "Retry",
|
||||
"profileProblemHandleInvalid": "A handle is 1 to 30 letters (a to z), digits or underscores.",
|
||||
"profileProblemHandleReserved": "This handle is kept for the instance.",
|
||||
"profileProblemHandleTaken": "Someone else has this handle.",
|
||||
"profileProblemPublished": "You have published: your handle can't change, nor your profile go.",
|
||||
"profileProblemDisplayNameTooLong": "At most 30 characters.",
|
||||
"profileProblemBioTooLong": "At most 500 characters.",
|
||||
"profileProblemTooManyLinks": "At most 4 links.",
|
||||
"profileProblemLinkInvalid": "Links start with https:// or http://.",
|
||||
"profileProblemAvatarInvalid": "This picture can't be used: choose a PNG, JPEG, WebP or GIF image.",
|
||||
"profileProblemTooManyLinkChecks": "The links were checked too often. Try again in an hour.",
|
||||
"profileProblemTooManyHandleChecks": "Too many handles were tried. Try again in an hour.",
|
||||
"publicProfilePicture": "Picture",
|
||||
"publicProfileChoosePicture": "Choose picture",
|
||||
"publicProfileRemovePicture": "Remove picture",
|
||||
"publicProfilePictureType": "Images",
|
||||
"publicProfilePictureLater": "Choose your handle to add a picture.",
|
||||
|
||||
"settingsTitle": "Settings",
|
||||
"settingsAccount": "Account",
|
||||
|
|
|
|||
|
|
@ -2704,18 +2704,6 @@ abstract class AppLocalizations {
|
|||
/// **'Studysets that others share will appear here.'**
|
||||
String get exploreEmpty;
|
||||
|
||||
/// No description provided for @profileTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Profile'**
|
||||
String get profileTitle;
|
||||
|
||||
/// No description provided for @profileTabProfile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Profile'**
|
||||
String get profileTabProfile;
|
||||
|
||||
/// No description provided for @profileTabStatistics.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2812,42 +2800,6 @@ abstract class AppLocalizations {
|
|||
/// **'Nothing studied yet. Your study time and ratings show here once you study flashcards.'**
|
||||
String get statisticsNothingYet;
|
||||
|
||||
/// No description provided for @profileSettingsTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get profileSettingsTooltip;
|
||||
|
||||
/// No description provided for @profileIdentified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your progress is kept with your account.'**
|
||||
String get profileIdentified;
|
||||
|
||||
/// No description provided for @profileSignInWays.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Ways to sign in'**
|
||||
String get profileSignInWays;
|
||||
|
||||
/// No description provided for @profileWayEmail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Email address'**
|
||||
String get profileWayEmail;
|
||||
|
||||
/// No description provided for @profileWayLinked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Signs you in'**
|
||||
String get profileWayLinked;
|
||||
|
||||
/// No description provided for @publicProfileTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Public profile'**
|
||||
String get publicProfileTitle;
|
||||
|
||||
/// No description provided for @profileSheetTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2860,354 +2812,12 @@ abstract class AppLocalizations {
|
|||
/// **'Nobody on this instance has this handle.'**
|
||||
String get profileSheetNotFound;
|
||||
|
||||
/// No description provided for @profileSheetLinkVerified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Links back to this profile'**
|
||||
String get profileSheetLinkVerified;
|
||||
|
||||
/// No description provided for @profileSheetUnreachable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The profile couldn’t be loaded. Check your connection.'**
|
||||
String get profileSheetUnreachable;
|
||||
|
||||
/// No description provided for @publicProfileIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'How others see you once you publish: the handle they find you by, your name, a few words and links. It saves as you write, once you have chosen your handle.'**
|
||||
String get publicProfileIntro;
|
||||
|
||||
/// No description provided for @publicProfileDisplayName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Display name'**
|
||||
String get publicProfileDisplayName;
|
||||
|
||||
/// No description provided for @publicProfileDisplayNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The name shown beside your handle'**
|
||||
String get publicProfileDisplayNameHint;
|
||||
|
||||
/// No description provided for @publicProfileHandle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Handle'**
|
||||
String get publicProfileHandle;
|
||||
|
||||
/// No description provided for @publicProfileHandleHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Letters, digits and underscores, up to 30'**
|
||||
String get publicProfileHandleHint;
|
||||
|
||||
/// No description provided for @publicProfileAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Others find you as @{address}'**
|
||||
String publicProfileAddress(String address);
|
||||
|
||||
/// No description provided for @publicProfileAddressPrivate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Private until you fill in your profile or publish something; then others find you as @{address}.'**
|
||||
String publicProfileAddressPrivate(String address);
|
||||
|
||||
/// No description provided for @publicProfileHandleFixed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fixed since you published: others refer to it.'**
|
||||
String get publicProfileHandleFixed;
|
||||
|
||||
/// No description provided for @publicProfileBio.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bio'**
|
||||
String get publicProfileBio;
|
||||
|
||||
/// No description provided for @publicProfileLinks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Links'**
|
||||
String get publicProfileLinks;
|
||||
|
||||
/// No description provided for @publicProfileLinksHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your site or other accounts, up to 4.'**
|
||||
String get publicProfileLinksHint;
|
||||
|
||||
/// No description provided for @publicProfileLinkHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'https://…'**
|
||||
String get publicProfileLinkHint;
|
||||
|
||||
/// No description provided for @publicProfileAddLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add link'**
|
||||
String get publicProfileAddLink;
|
||||
|
||||
/// No description provided for @publicProfilePage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your profile page: {url}'**
|
||||
String publicProfilePage(String url);
|
||||
|
||||
/// No description provided for @publicProfilePageCopy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy the address'**
|
||||
String get publicProfilePageCopy;
|
||||
|
||||
/// No description provided for @publicProfilePageCopied.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Address copied.'**
|
||||
String get publicProfilePageCopied;
|
||||
|
||||
/// No description provided for @publicProfileVerifyHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A link gets a checkmark once its page links back to your profile page with rel=\"me\", as Mastodon checks links.'**
|
||||
String get publicProfileVerifyHint;
|
||||
|
||||
/// No description provided for @publicProfileCheckLinks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Check links'**
|
||||
String get publicProfileCheckLinks;
|
||||
|
||||
/// No description provided for @publicProfileLinkVerified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Links back to your profile'**
|
||||
String get publicProfileLinkVerified;
|
||||
|
||||
/// No description provided for @publicProfileLinksChecked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{Checked: {verified} of 1 link links back to your profile.} other{Checked: {verified} of {count} links link back to your profile.}}'**
|
||||
String publicProfileLinksChecked(int count, int verified);
|
||||
|
||||
/// No description provided for @publicProfileRemoveLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove link'**
|
||||
String get publicProfileRemoveLink;
|
||||
|
||||
/// No description provided for @publicProfileSaving.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Saving…'**
|
||||
String get publicProfileSaving;
|
||||
|
||||
/// No description provided for @publicProfileSavedNow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Saved'**
|
||||
String get publicProfileSavedNow;
|
||||
|
||||
/// No description provided for @publicProfileHandleChoose.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose a handle'**
|
||||
String get publicProfileHandleChoose;
|
||||
|
||||
/// No description provided for @publicProfileHandleFirst.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose your handle first: your profile is made with it, and saves as you write from then on.'**
|
||||
String get publicProfileHandleFirst;
|
||||
|
||||
/// No description provided for @publicProfileHandleChooseTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose your handle'**
|
||||
String get publicProfileHandleChooseTitle;
|
||||
|
||||
/// No description provided for @publicProfileHandleChangeTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change your handle'**
|
||||
String get publicProfileHandleChangeTitle;
|
||||
|
||||
/// No description provided for @publicProfileHandleDialogText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Others find you by your handle. You can change it until you first publish a studyset.'**
|
||||
String get publicProfileHandleDialogText;
|
||||
|
||||
/// No description provided for @publicProfileHandleChecking.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Checking…'**
|
||||
String get publicProfileHandleChecking;
|
||||
|
||||
/// No description provided for @publicProfileHandleFree.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'@{handle} is free'**
|
||||
String publicProfileHandleFree(String handle);
|
||||
|
||||
/// No description provided for @publicProfileHandleYours.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This is your handle'**
|
||||
String get publicProfileHandleYours;
|
||||
|
||||
/// No description provided for @publicProfileHandleConfirmChoose.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose'**
|
||||
String get publicProfileHandleConfirmChoose;
|
||||
|
||||
/// No description provided for @publicProfileHandleConfirmChange.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change'**
|
||||
String get publicProfileHandleConfirmChange;
|
||||
|
||||
/// No description provided for @publicProfileRemove.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove profile'**
|
||||
String get publicProfileRemove;
|
||||
|
||||
/// No description provided for @publicProfileRemoveTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove your public profile?'**
|
||||
String get publicProfileRemoveTitle;
|
||||
|
||||
/// No description provided for @publicProfileRemoveMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your display name, bio, links and picture are removed. Your handle stays yours and private until you fill in your profile again or publish something.'**
|
||||
String get publicProfileRemoveMessage;
|
||||
|
||||
/// No description provided for @publicProfileRemoved.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Profile removed.'**
|
||||
String get publicProfileRemoved;
|
||||
|
||||
/// No description provided for @publicProfileFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Couldn\'t reach the instance. Try again.'**
|
||||
String get publicProfileFailed;
|
||||
|
||||
/// No description provided for @publicProfileUnreachable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your public profile can\'t be reached right now.'**
|
||||
String get publicProfileUnreachable;
|
||||
|
||||
/// No description provided for @publicProfileRetry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Retry'**
|
||||
String get publicProfileRetry;
|
||||
|
||||
/// No description provided for @profileProblemHandleInvalid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A handle is 1 to 30 letters (a to z), digits or underscores.'**
|
||||
String get profileProblemHandleInvalid;
|
||||
|
||||
/// No description provided for @profileProblemHandleReserved.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This handle is kept for the instance.'**
|
||||
String get profileProblemHandleReserved;
|
||||
|
||||
/// No description provided for @profileProblemHandleTaken.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Someone else has this handle.'**
|
||||
String get profileProblemHandleTaken;
|
||||
|
||||
/// No description provided for @profileProblemPublished.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You have published: your handle can\'t change, nor your profile go.'**
|
||||
String get profileProblemPublished;
|
||||
|
||||
/// No description provided for @profileProblemDisplayNameTooLong.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'At most 30 characters.'**
|
||||
String get profileProblemDisplayNameTooLong;
|
||||
|
||||
/// No description provided for @profileProblemBioTooLong.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'At most 500 characters.'**
|
||||
String get profileProblemBioTooLong;
|
||||
|
||||
/// No description provided for @profileProblemTooManyLinks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'At most 4 links.'**
|
||||
String get profileProblemTooManyLinks;
|
||||
|
||||
/// No description provided for @profileProblemLinkInvalid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Links start with https:// or http://.'**
|
||||
String get profileProblemLinkInvalid;
|
||||
|
||||
/// No description provided for @profileProblemAvatarInvalid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This picture can\'t be used: choose a PNG, JPEG, WebP or GIF image.'**
|
||||
String get profileProblemAvatarInvalid;
|
||||
|
||||
/// No description provided for @profileProblemTooManyLinkChecks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The links were checked too often. Try again in an hour.'**
|
||||
String get profileProblemTooManyLinkChecks;
|
||||
|
||||
/// No description provided for @profileProblemTooManyHandleChecks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Too many handles were tried. Try again in an hour.'**
|
||||
String get profileProblemTooManyHandleChecks;
|
||||
|
||||
/// No description provided for @publicProfilePicture.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Picture'**
|
||||
String get publicProfilePicture;
|
||||
|
||||
/// No description provided for @publicProfileChoosePicture.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose picture'**
|
||||
String get publicProfileChoosePicture;
|
||||
|
||||
/// No description provided for @publicProfileRemovePicture.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove picture'**
|
||||
String get publicProfileRemovePicture;
|
||||
|
||||
/// No description provided for @publicProfilePictureType.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Images'**
|
||||
String get publicProfilePictureType;
|
||||
|
||||
/// No description provided for @publicProfilePictureLater.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose your handle to add a picture.'**
|
||||
String get publicProfilePictureLater;
|
||||
|
||||
/// No description provided for @settingsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1639,12 +1639,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get exploreEmpty => 'Lernsets, die andere teilen, erscheinen hier.';
|
||||
|
||||
@override
|
||||
String get profileTitle => 'Profil';
|
||||
|
||||
@override
|
||||
String get profileTabProfile => 'Profil';
|
||||
|
||||
@override
|
||||
String get profileTabStatistics => 'Statistik';
|
||||
|
||||
|
|
@ -1708,25 +1702,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get statisticsNothingYet =>
|
||||
'Noch nichts gelernt. Deine Lernzeit und Bewertungen erscheinen hier, sobald du Karteikarten lernst.';
|
||||
|
||||
@override
|
||||
String get profileSettingsTooltip => 'Einstellungen';
|
||||
|
||||
@override
|
||||
String get profileIdentified =>
|
||||
'Dein Fortschritt wird in deinem Konto gespeichert.';
|
||||
|
||||
@override
|
||||
String get profileSignInWays => 'Anmeldewege';
|
||||
|
||||
@override
|
||||
String get profileWayEmail => 'E-Mail-Adresse';
|
||||
|
||||
@override
|
||||
String get profileWayLinked => 'Meldet dich an';
|
||||
|
||||
@override
|
||||
String get publicProfileTitle => 'Öffentliches Profil';
|
||||
|
||||
@override
|
||||
String get profileSheetTitle => 'Profil';
|
||||
|
||||
|
|
@ -1734,217 +1709,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get profileSheetNotFound =>
|
||||
'Niemand auf dieser Instanz hat diesen Handle.';
|
||||
|
||||
@override
|
||||
String get profileSheetLinkVerified => 'Verlinkt zurück auf dieses Profil';
|
||||
|
||||
@override
|
||||
String get profileSheetUnreachable =>
|
||||
'Das Profil konnte nicht geladen werden. Prüfe deine Verbindung.';
|
||||
|
||||
@override
|
||||
String get publicProfileIntro =>
|
||||
'Wie andere dich sehen, sobald du veröffentlichst: das Handle, unter dem sie dich finden, dein Name, ein paar Worte und Links. Es speichert sich beim Schreiben, sobald du dein Handle gewählt hast.';
|
||||
|
||||
@override
|
||||
String get publicProfileDisplayName => 'Anzeigename';
|
||||
|
||||
@override
|
||||
String get publicProfileDisplayNameHint => 'Der Name neben deinem Handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandle => 'Handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleHint =>
|
||||
'Buchstaben, Ziffern und Unterstriche, bis zu 30';
|
||||
|
||||
@override
|
||||
String publicProfileAddress(String address) {
|
||||
return 'Andere finden dich als @$address';
|
||||
}
|
||||
|
||||
@override
|
||||
String publicProfileAddressPrivate(String address) {
|
||||
return 'Privat, bis du dein Profil ausfüllst oder etwas veröffentlichst; dann finden dich andere als @$address.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publicProfileHandleFixed =>
|
||||
'Fest, seit du veröffentlicht hast: Andere verweisen darauf.';
|
||||
|
||||
@override
|
||||
String get publicProfileBio => 'Über dich';
|
||||
|
||||
@override
|
||||
String get publicProfileLinks => 'Links';
|
||||
|
||||
@override
|
||||
String get publicProfileLinksHint =>
|
||||
'Deine Website oder andere Konten, bis zu 4.';
|
||||
|
||||
@override
|
||||
String get publicProfileLinkHint => 'https://…';
|
||||
|
||||
@override
|
||||
String get publicProfileAddLink => 'Link hinzufügen';
|
||||
|
||||
@override
|
||||
String publicProfilePage(String url) {
|
||||
return 'Deine Profilseite: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publicProfilePageCopy => 'Adresse kopieren';
|
||||
|
||||
@override
|
||||
String get publicProfilePageCopied => 'Adresse kopiert.';
|
||||
|
||||
@override
|
||||
String get publicProfileVerifyHint =>
|
||||
'Ein Link bekommt ein Häkchen, sobald seine Seite mit rel=\"me\" auf deine Profilseite zurückverlinkt, wie Mastodon Links prüft.';
|
||||
|
||||
@override
|
||||
String get publicProfileCheckLinks => 'Links prüfen';
|
||||
|
||||
@override
|
||||
String get publicProfileLinkVerified => 'Verlinkt zurück auf dein Profil';
|
||||
|
||||
@override
|
||||
String publicProfileLinksChecked(int count, int verified) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other:
|
||||
'Geprüft: $verified von $count Links verlinken zurück auf dein Profil.',
|
||||
one: 'Geprüft: $verified von 1 Link verlinkt zurück auf dein Profil.',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publicProfileRemoveLink => 'Link entfernen';
|
||||
|
||||
@override
|
||||
String get publicProfileSaving => 'Wird gespeichert …';
|
||||
|
||||
@override
|
||||
String get publicProfileSavedNow => 'Gespeichert';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleChoose => 'Wähle ein Handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleFirst =>
|
||||
'Wähle zuerst dein Handle: Damit wird dein Profil angelegt, und ab dann speichert es sich beim Schreiben.';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleChooseTitle => 'Wähle dein Handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleChangeTitle => 'Ändere dein Handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleDialogText =>
|
||||
'Andere finden dich unter deinem Handle. Du kannst es ändern, bis du zum ersten Mal ein Lernset veröffentlichst.';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleChecking => 'Wird geprüft …';
|
||||
|
||||
@override
|
||||
String publicProfileHandleFree(String handle) {
|
||||
return '@$handle ist frei';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publicProfileHandleYours => 'Das ist dein Handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleConfirmChoose => 'Wählen';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleConfirmChange => 'Ändern';
|
||||
|
||||
@override
|
||||
String get publicProfileRemove => 'Profil entfernen';
|
||||
|
||||
@override
|
||||
String get publicProfileRemoveTitle => 'Dein öffentliches Profil entfernen?';
|
||||
|
||||
@override
|
||||
String get publicProfileRemoveMessage =>
|
||||
'Anzeigename, Text über dich, Links und Bild werden entfernt. Dein Handle bleibt deins und privat, bis du dein Profil wieder ausfüllst oder etwas veröffentlichst.';
|
||||
|
||||
@override
|
||||
String get publicProfileRemoved => 'Profil entfernt.';
|
||||
|
||||
@override
|
||||
String get publicProfileFailed =>
|
||||
'Die Instanz ist nicht erreichbar. Versuche es erneut.';
|
||||
|
||||
@override
|
||||
String get publicProfileUnreachable =>
|
||||
'Dein öffentliches Profil ist gerade nicht erreichbar.';
|
||||
|
||||
@override
|
||||
String get publicProfileRetry => 'Erneut versuchen';
|
||||
|
||||
@override
|
||||
String get profileProblemHandleInvalid =>
|
||||
'Ein Handle besteht aus 1 bis 30 Buchstaben (a bis z), Ziffern oder Unterstrichen.';
|
||||
|
||||
@override
|
||||
String get profileProblemHandleReserved =>
|
||||
'Dieses Handle ist der Instanz vorbehalten.';
|
||||
|
||||
@override
|
||||
String get profileProblemHandleTaken =>
|
||||
'Dieses Handle hat schon jemand anderes.';
|
||||
|
||||
@override
|
||||
String get profileProblemPublished =>
|
||||
'Du hast veröffentlicht: Dein Handle kann sich nicht mehr ändern und dein Profil nicht mehr gehen.';
|
||||
|
||||
@override
|
||||
String get profileProblemDisplayNameTooLong => 'Höchstens 30 Zeichen.';
|
||||
|
||||
@override
|
||||
String get profileProblemBioTooLong => 'Höchstens 500 Zeichen.';
|
||||
|
||||
@override
|
||||
String get profileProblemTooManyLinks => 'Höchstens 4 Links.';
|
||||
|
||||
@override
|
||||
String get profileProblemLinkInvalid =>
|
||||
'Links beginnen mit https:// oder http://.';
|
||||
|
||||
@override
|
||||
String get profileProblemAvatarInvalid =>
|
||||
'Dieses Bild lässt sich nicht verwenden: Wähle ein PNG-, JPEG-, WebP- oder GIF-Bild.';
|
||||
|
||||
@override
|
||||
String get profileProblemTooManyLinkChecks =>
|
||||
'Die Links wurden zu oft geprüft. Versuche es in einer Stunde noch einmal.';
|
||||
|
||||
@override
|
||||
String get profileProblemTooManyHandleChecks =>
|
||||
'Es wurden zu viele Handles ausprobiert. Versuche es in einer Stunde noch einmal.';
|
||||
|
||||
@override
|
||||
String get publicProfilePicture => 'Bild';
|
||||
|
||||
@override
|
||||
String get publicProfileChoosePicture => 'Bild wählen';
|
||||
|
||||
@override
|
||||
String get publicProfileRemovePicture => 'Bild entfernen';
|
||||
|
||||
@override
|
||||
String get publicProfilePictureType => 'Bilder';
|
||||
|
||||
@override
|
||||
String get publicProfilePictureLater =>
|
||||
'Wähle dein Handle, um ein Bild hinzuzufügen.';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Einstellungen';
|
||||
|
||||
|
|
|
|||
|
|
@ -1624,12 +1624,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get exploreEmpty => 'Studysets that others share will appear here.';
|
||||
|
||||
@override
|
||||
String get profileTitle => 'Profile';
|
||||
|
||||
@override
|
||||
String get profileTabProfile => 'Profile';
|
||||
|
||||
@override
|
||||
String get profileTabStatistics => 'Statistics';
|
||||
|
||||
|
|
@ -1693,238 +1687,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get statisticsNothingYet =>
|
||||
'Nothing studied yet. Your study time and ratings show here once you study flashcards.';
|
||||
|
||||
@override
|
||||
String get profileSettingsTooltip => 'Settings';
|
||||
|
||||
@override
|
||||
String get profileIdentified => 'Your progress is kept with your account.';
|
||||
|
||||
@override
|
||||
String get profileSignInWays => 'Ways to sign in';
|
||||
|
||||
@override
|
||||
String get profileWayEmail => 'Email address';
|
||||
|
||||
@override
|
||||
String get profileWayLinked => 'Signs you in';
|
||||
|
||||
@override
|
||||
String get publicProfileTitle => 'Public profile';
|
||||
|
||||
@override
|
||||
String get profileSheetTitle => 'Profile';
|
||||
|
||||
@override
|
||||
String get profileSheetNotFound => 'Nobody on this instance has this handle.';
|
||||
|
||||
@override
|
||||
String get profileSheetLinkVerified => 'Links back to this profile';
|
||||
|
||||
@override
|
||||
String get profileSheetUnreachable =>
|
||||
'The profile couldn’t be loaded. Check your connection.';
|
||||
|
||||
@override
|
||||
String get publicProfileIntro =>
|
||||
'How others see you once you publish: the handle they find you by, your name, a few words and links. It saves as you write, once you have chosen your handle.';
|
||||
|
||||
@override
|
||||
String get publicProfileDisplayName => 'Display name';
|
||||
|
||||
@override
|
||||
String get publicProfileDisplayNameHint =>
|
||||
'The name shown beside your handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandle => 'Handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleHint =>
|
||||
'Letters, digits and underscores, up to 30';
|
||||
|
||||
@override
|
||||
String publicProfileAddress(String address) {
|
||||
return 'Others find you as @$address';
|
||||
}
|
||||
|
||||
@override
|
||||
String publicProfileAddressPrivate(String address) {
|
||||
return 'Private until you fill in your profile or publish something; then others find you as @$address.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publicProfileHandleFixed =>
|
||||
'Fixed since you published: others refer to it.';
|
||||
|
||||
@override
|
||||
String get publicProfileBio => 'Bio';
|
||||
|
||||
@override
|
||||
String get publicProfileLinks => 'Links';
|
||||
|
||||
@override
|
||||
String get publicProfileLinksHint => 'Your site or other accounts, up to 4.';
|
||||
|
||||
@override
|
||||
String get publicProfileLinkHint => 'https://…';
|
||||
|
||||
@override
|
||||
String get publicProfileAddLink => 'Add link';
|
||||
|
||||
@override
|
||||
String publicProfilePage(String url) {
|
||||
return 'Your profile page: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publicProfilePageCopy => 'Copy the address';
|
||||
|
||||
@override
|
||||
String get publicProfilePageCopied => 'Address copied.';
|
||||
|
||||
@override
|
||||
String get publicProfileVerifyHint =>
|
||||
'A link gets a checkmark once its page links back to your profile page with rel=\"me\", as Mastodon checks links.';
|
||||
|
||||
@override
|
||||
String get publicProfileCheckLinks => 'Check links';
|
||||
|
||||
@override
|
||||
String get publicProfileLinkVerified => 'Links back to your profile';
|
||||
|
||||
@override
|
||||
String publicProfileLinksChecked(int count, int verified) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Checked: $verified of $count links link back to your profile.',
|
||||
one: 'Checked: $verified of 1 link links back to your profile.',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publicProfileRemoveLink => 'Remove link';
|
||||
|
||||
@override
|
||||
String get publicProfileSaving => 'Saving…';
|
||||
|
||||
@override
|
||||
String get publicProfileSavedNow => 'Saved';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleChoose => 'Choose a handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleFirst =>
|
||||
'Choose your handle first: your profile is made with it, and saves as you write from then on.';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleChooseTitle => 'Choose your handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleChangeTitle => 'Change your handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleDialogText =>
|
||||
'Others find you by your handle. You can change it until you first publish a studyset.';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleChecking => 'Checking…';
|
||||
|
||||
@override
|
||||
String publicProfileHandleFree(String handle) {
|
||||
return '@$handle is free';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publicProfileHandleYours => 'This is your handle';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleConfirmChoose => 'Choose';
|
||||
|
||||
@override
|
||||
String get publicProfileHandleConfirmChange => 'Change';
|
||||
|
||||
@override
|
||||
String get publicProfileRemove => 'Remove profile';
|
||||
|
||||
@override
|
||||
String get publicProfileRemoveTitle => 'Remove your public profile?';
|
||||
|
||||
@override
|
||||
String get publicProfileRemoveMessage =>
|
||||
'Your display name, bio, links and picture are removed. Your handle stays yours and private until you fill in your profile again or publish something.';
|
||||
|
||||
@override
|
||||
String get publicProfileRemoved => 'Profile removed.';
|
||||
|
||||
@override
|
||||
String get publicProfileFailed => 'Couldn\'t reach the instance. Try again.';
|
||||
|
||||
@override
|
||||
String get publicProfileUnreachable =>
|
||||
'Your public profile can\'t be reached right now.';
|
||||
|
||||
@override
|
||||
String get publicProfileRetry => 'Retry';
|
||||
|
||||
@override
|
||||
String get profileProblemHandleInvalid =>
|
||||
'A handle is 1 to 30 letters (a to z), digits or underscores.';
|
||||
|
||||
@override
|
||||
String get profileProblemHandleReserved =>
|
||||
'This handle is kept for the instance.';
|
||||
|
||||
@override
|
||||
String get profileProblemHandleTaken => 'Someone else has this handle.';
|
||||
|
||||
@override
|
||||
String get profileProblemPublished =>
|
||||
'You have published: your handle can\'t change, nor your profile go.';
|
||||
|
||||
@override
|
||||
String get profileProblemDisplayNameTooLong => 'At most 30 characters.';
|
||||
|
||||
@override
|
||||
String get profileProblemBioTooLong => 'At most 500 characters.';
|
||||
|
||||
@override
|
||||
String get profileProblemTooManyLinks => 'At most 4 links.';
|
||||
|
||||
@override
|
||||
String get profileProblemLinkInvalid =>
|
||||
'Links start with https:// or http://.';
|
||||
|
||||
@override
|
||||
String get profileProblemAvatarInvalid =>
|
||||
'This picture can\'t be used: choose a PNG, JPEG, WebP or GIF image.';
|
||||
|
||||
@override
|
||||
String get profileProblemTooManyLinkChecks =>
|
||||
'The links were checked too often. Try again in an hour.';
|
||||
|
||||
@override
|
||||
String get profileProblemTooManyHandleChecks =>
|
||||
'Too many handles were tried. Try again in an hour.';
|
||||
|
||||
@override
|
||||
String get publicProfilePicture => 'Picture';
|
||||
|
||||
@override
|
||||
String get publicProfileChoosePicture => 'Choose picture';
|
||||
|
||||
@override
|
||||
String get publicProfileRemovePicture => 'Remove picture';
|
||||
|
||||
@override
|
||||
String get publicProfilePictureType => 'Images';
|
||||
|
||||
@override
|
||||
String get publicProfilePictureLater =>
|
||||
'Choose your handle to add a picture.';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Settings';
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import 'package:halkyon_learn_flutter/src/core/asset/data/sync_asset_gateway.dar
|
|||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/files/file_picker.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_checks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/privacy/data/content_keys.dart';
|
||||
|
|
@ -73,8 +72,6 @@ import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/sync_pdf_sourc
|
|||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platforms.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_run_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/data/serverpod_profile_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/publish/data/published_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/publish/data/serverpod_publish_gateway.dart';
|
||||
|
|
@ -158,7 +155,11 @@ class Dependencies({
|
|||
required final SoundPlayer sounds,
|
||||
|
||||
/// Lets the person pick files to import.
|
||||
required final FilePicker files,
|
||||
@override required final FilePicker files,
|
||||
|
||||
/// People's public profiles on the instance, the person's own among them:
|
||||
/// one for every Halkyon app (decided 2026-11-02).
|
||||
@override required final PublicProfileRepository profiles,
|
||||
|
||||
/// Lets the person keep files the app made, such as exports.
|
||||
@override required final FileSaver fileSaver,
|
||||
|
|
@ -305,6 +306,9 @@ class Dependencies({
|
|||
instanceOperators: instanceOperators,
|
||||
assistants: assistants,
|
||||
assistantOperators: assistantOperators,
|
||||
profiles: PublicProfileRepository(
|
||||
BasePublicProfileGateway(client.modules.base),
|
||||
),
|
||||
sourceCodeUrl: () async => (await client.instance.info()).sourceCodeUrl,
|
||||
dictionaries: DictionaryRepository(
|
||||
ServerpodDictionaryGateway(client),
|
||||
|
|
@ -627,7 +631,6 @@ Future<PersonDependencies> _openPersonData(
|
|||
privateOffer: FilePrivateOfferStore(
|
||||
File(p.join(folder.path, 'private_offer')),
|
||||
),
|
||||
profile: ProfileRepository(ServerpodProfileGateway(client)),
|
||||
publishing: publishing,
|
||||
explore: ExploreRepository(
|
||||
ServerpodExploreGateway(client),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart
|
|||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_run_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||
|
|
@ -107,9 +106,6 @@ class PersonDependencies({
|
|||
/// answered on this device (ADR 0019, D4).
|
||||
required final PrivateOfferStore privateOffer,
|
||||
|
||||
/// The person's public profile, on the instance.
|
||||
required final ProfileRepository profile,
|
||||
|
||||
/// Publishing the person's studysets, on the instance.
|
||||
required final PublishRepository publishing,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
|
||||
/// A file the person picked: its name and its content.
|
||||
typedef PickedFile = ({String name, Uint8List bytes});
|
||||
|
||||
/// Lets the person pick a file of this device.
|
||||
abstract interface class FilePicker() {
|
||||
/// The file the person picked, of a type [label] names with [extensions];
|
||||
/// null if they picked none.
|
||||
Future<PickedFile?> pick({
|
||||
required String label,
|
||||
required List<String> extensions,
|
||||
});
|
||||
|
||||
/// The files the person picked, of a type [label] names with
|
||||
/// [extensions]; none if they picked none.
|
||||
Future<List<PickedFile>> pickMany({
|
||||
required String label,
|
||||
required List<String> extensions,
|
||||
});
|
||||
}
|
||||
|
||||
/// [FilePicker] with the platform's own dialog (`file_selector`).
|
||||
class FileSelectorFilePicker() implements FilePicker {
|
||||
@override
|
||||
Future<PickedFile?> pick({
|
||||
required String label,
|
||||
required List<String> extensions,
|
||||
}) async {
|
||||
final file = await openFile(
|
||||
acceptedTypeGroups: _typeGroups(label, extensions),
|
||||
);
|
||||
return file == null
|
||||
? null
|
||||
: (name: file.name, bytes: await file.readAsBytes());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PickedFile>> pickMany({
|
||||
required String label,
|
||||
required List<String> extensions,
|
||||
}) async => [
|
||||
for (final file in await openFiles(
|
||||
acceptedTypeGroups: _typeGroups(label, extensions),
|
||||
))
|
||||
(name: file.name, bytes: await file.readAsBytes()),
|
||||
];
|
||||
|
||||
List<XTypeGroup> _typeGroups(String label, List<String> extensions) => [
|
||||
// Android filters by media type, which extensions like Anki's lack, so
|
||||
// it offers every file there; what isn't one is refused later.
|
||||
if (!Platform.isAndroid)
|
||||
XTypeGroup(
|
||||
label: label,
|
||||
extensions: extensions,
|
||||
uniformTypeIdentifiers: const ['public.data'],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:halkyon_learn_flutter/src/core/files/file_picker.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_files.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topic.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_engine.dart';
|
||||
|
|
|
|||
|
|
@ -1,128 +1,42 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart' as app;
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/discussion/widget/discussion_inbox.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/widget/inbox_section.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/widget/public_profile_section.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/statistics/widget/statistics_view.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The profile tab, in two tabs as Vaia has it (decided 2026-10-12): the
|
||||
/// profile (the ways the person signs in, SimpleLogin, ADR 0020, and their
|
||||
/// password and authenticators, ADR 0017) and the person's statistics.
|
||||
/// Settings are behind the gear. The tab row folds into the title as the
|
||||
/// content scrolls ([FoldingTabs], as Strava's).
|
||||
/// The profile tab: every Halkyon app's Profile (halkyon_app's
|
||||
/// [app.HalkyonProfile], decided 2026-11-02), the person's own tab first
|
||||
/// (their account and how they sign in, Learn's inbox of discussions and
|
||||
/// feedback, and their public profile), then their statistics, in two
|
||||
/// tabs as Vaia has it (decided 2026-10-12). Settings are behind the gear.
|
||||
class const ProfileScreen({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
SignInWaysCubit(Dependencies.of(context).auth)..load(),
|
||||
child: TopGradient(
|
||||
color: Brand.primary,
|
||||
background: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: FoldingTabs(
|
||||
title: l10n.profileTitle,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const WaitingAccountsBadge(
|
||||
child: SyncTroubleBadge(child: Icon(LucideIcons.settings)),
|
||||
),
|
||||
tooltip: l10n.profileSettingsTooltip,
|
||||
onPressed: () => context.push(RoutePaths.settings),
|
||||
),
|
||||
],
|
||||
tabs: [
|
||||
FoldingTab(label: l10n.profileTabProfile, body: const _Account()),
|
||||
FoldingTab(
|
||||
label: l10n.profileTabStatistics,
|
||||
body: const StatisticsView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class const _Account() extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
// On the screen itself, in the middle, as wide as readable text.
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: readableContentMaxWidth),
|
||||
// A card for each part, as Settings groups its own.
|
||||
child: ListView(
|
||||
// Room at the end for the navigation bar floating over the foot.
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12 + MediaQuery.paddingOf(context).bottom,
|
||||
return app.HalkyonProfile(
|
||||
color: Brand.primary,
|
||||
background: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const app.WaitingAccountsBadge(
|
||||
child: app.SyncTroubleBadge(child: Icon(LucideIcons.settings)),
|
||||
),
|
||||
children: [
|
||||
SectionCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.settingsAccount,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(l10n.profileIdentified),
|
||||
const _SignInWays(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SectionCard(
|
||||
child: InboxSection(discussions: DiscussionInbox()),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SectionCard(child: PublicProfileSection()),
|
||||
],
|
||||
tooltip: l10n.settingsTitle,
|
||||
onPressed: () => context.push(RoutePaths.settings),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
sections: const [InboxSection(discussions: DiscussionInbox())],
|
||||
tabs: [
|
||||
FoldingTab(
|
||||
label: l10n.profileTabStatistics,
|
||||
body: const StatisticsView(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// How the person signs in: an email address, and SimpleLogin (ADR 0020,
|
||||
/// D21), with the password and authenticators beneath.
|
||||
class const _SignInWays() extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final ways = context.watch<SignInWaysCubit>().state;
|
||||
Widget way(String label, String linkedAs) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(label),
|
||||
subtitle: Text(linkedAs, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
trailing: Icon(LucideIcons.circleCheck, color: theme.colorScheme.primary),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Text(l10n.profileSignInWays, style: theme.textTheme.titleSmall),
|
||||
if (ways.ways.email)
|
||||
way(l10n.profileWayEmail, ways.emailAddress ?? l10n.profileWayLinked),
|
||||
// SimpleLogin (ADR 0020, D21): once added, one more way in here.
|
||||
const SimpleLoginSection(gapAbove: 16),
|
||||
const SizedBox(height: 16),
|
||||
// The password and authenticators, and setting them up (ADR 0017),
|
||||
// which brings the address.
|
||||
const AccountSecuritySection(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/content_languages.dart';
|
||||
|
|
@ -8,7 +9,6 @@ import 'package:halkyon_learn_flutter/src/feature/explore/bloc/explore_bloc.dart
|
|||
import 'package:halkyon_learn_flutter/src/feature/explore/model/explore_studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/explore/widget/explore_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/widget/feedback_threads.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/widget/profile_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/license_footer.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
|
||||
|
||||
/// Whether the handle being typed could be the person's.
|
||||
enum HandleCheck() {
|
||||
/// Nothing typed, or the person's own handle: nothing to say.
|
||||
none,
|
||||
|
||||
/// Being asked of the instance.
|
||||
checking,
|
||||
|
||||
/// Free.
|
||||
free,
|
||||
|
||||
/// Refused: [HandleCheckState.problem] says why.
|
||||
refused,
|
||||
|
||||
/// The instance couldn't be asked; saving will say.
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// The handle typed, and what the instance says of it.
|
||||
final class const HandleCheckState({
|
||||
final String handle = '',
|
||||
final HandleCheck check = HandleCheck.none,
|
||||
final ProfileProblem? problem,
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [handle, check, problem];
|
||||
}
|
||||
|
||||
/// Asks the instance whether a handle could be the person's while it is
|
||||
/// typed (decided 2026-10-12), once typing pauses, and says only what it
|
||||
/// says of the handle typed last. The person's own handle needs
|
||||
/// no asking.
|
||||
class HandleCheckCubit(
|
||||
final ProfileRepository _profiles, {
|
||||
final String? _current,
|
||||
}) extends Cubit<HandleCheckState> {
|
||||
this : super(HandleCheckState(handle: _current ?? ''));
|
||||
|
||||
/// How long typing must pause before the instance is asked.
|
||||
static const pause = Duration(milliseconds: 300);
|
||||
|
||||
Timer? _pause;
|
||||
|
||||
/// The person typed [handle].
|
||||
void typed(String handle) {
|
||||
_pause?.cancel();
|
||||
final trimmed = handle.trim();
|
||||
if (trimmed.isEmpty || trimmed == _current) {
|
||||
emit(HandleCheckState(handle: trimmed));
|
||||
return;
|
||||
}
|
||||
emit(HandleCheckState(handle: trimmed, check: HandleCheck.checking));
|
||||
_pause = Timer(pause, () => unawaited(_ask(trimmed)));
|
||||
}
|
||||
|
||||
Future<void> _ask(String handle) async {
|
||||
HandleCheckState answer;
|
||||
try {
|
||||
answer = switch (await _profiles.checkHandle(handle)) {
|
||||
null => HandleCheckState(handle: handle, check: HandleCheck.free),
|
||||
final problem => HandleCheckState(
|
||||
handle: handle,
|
||||
check: HandleCheck.refused,
|
||||
problem: problem,
|
||||
),
|
||||
};
|
||||
} on Object catch (error) {
|
||||
log('Checking a handle failed', name: 'Profile', error: error);
|
||||
answer = HandleCheckState(handle: handle, check: HandleCheck.unknown);
|
||||
}
|
||||
// Only what is still typed.
|
||||
if (!isClosed && state.handle == handle) {
|
||||
emit(answer);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_pause?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
|
||||
|
||||
/// The person's public profile: reading it, saving it as it is written
|
||||
/// (field by field, decided 2026-10-12), removing it, and having its links
|
||||
/// checked (ADR 0011): after a save that brought a link not yet checked, and
|
||||
/// when asked. A save asked for while
|
||||
/// another is on its way follows it, the latest of those waiting, so none
|
||||
/// is lost and none overtakes another. A handle chosen holds for every save
|
||||
/// after it until one with it went through or was refused for it, so a
|
||||
/// field's save can't take a new handle back.
|
||||
class PublicProfileBloc(final ProfileRepository _profiles)
|
||||
extends Bloc<PublicProfileEvent, PublicProfileState> {
|
||||
this : super(const PublicProfileStateLoading()) {
|
||||
on<PublicProfileEventStarted>(_onStarted);
|
||||
on<PublicProfileEventSaved>(_onSaved);
|
||||
on<PublicProfileEventRemoved>(_onRemoved);
|
||||
on<PublicProfileEventAvatarChosen>(
|
||||
(event, emit) => _change(emit, () => _profiles.setAvatar(event.picture)),
|
||||
);
|
||||
on<PublicProfileEventAvatarRemoved>(
|
||||
(event, emit) => _change(emit, _profiles.removeAvatar),
|
||||
);
|
||||
on<PublicProfileEventLinksChecked>(_onLinksChecked);
|
||||
}
|
||||
|
||||
/// The links checked by themselves already, or asked for.
|
||||
final _checkedLinks = <String>{};
|
||||
|
||||
Future<void> _onLinksChecked(
|
||||
PublicProfileEventLinksChecked event,
|
||||
Emitter<PublicProfileState> emit,
|
||||
) async {
|
||||
if (state case PublicProfileStateReady(
|
||||
:final profile?,
|
||||
saving: false,
|
||||
checkingLinks: false,
|
||||
)) {
|
||||
_checkedLinks.addAll(profile.links);
|
||||
emit(PublicProfileStateReady(profile: profile, checkingLinks: true));
|
||||
try {
|
||||
final checked = await _profiles.checkLinks();
|
||||
switch (state) {
|
||||
case PublicProfileStateReady(checkingLinks: true):
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: checked,
|
||||
outcome: event.asked ? PublicProfileOutcome.linksChecked : null,
|
||||
),
|
||||
);
|
||||
// Saved meanwhile: only the checks are new.
|
||||
case PublicProfileStateReady(
|
||||
profile: final current?,
|
||||
:final saving,
|
||||
:final problem,
|
||||
:final outcome,
|
||||
):
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: current.withVerifiedLinks(checked.verifiedLinks),
|
||||
saving: saving,
|
||||
problem: problem,
|
||||
outcome: outcome,
|
||||
),
|
||||
);
|
||||
case _:
|
||||
}
|
||||
} on ProfileRefused catch (refused) {
|
||||
emit(
|
||||
PublicProfileStateReady(profile: profile, problem: refused.problem),
|
||||
);
|
||||
} on Object catch (error) {
|
||||
log('Checking the links failed', name: 'Profile', error: error);
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: profile,
|
||||
outcome: event.asked ? PublicProfileOutcome.failed : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the profile through [change], as saving it does.
|
||||
Future<void> _change(
|
||||
Emitter<PublicProfileState> emit,
|
||||
Future<PublicProfile> Function() change,
|
||||
) async {
|
||||
if (state case PublicProfileStateReady(:final profile?, saving: false)) {
|
||||
emit(PublicProfileStateReady(profile: profile, saving: true));
|
||||
try {
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: await change(),
|
||||
outcome: PublicProfileOutcome.saved,
|
||||
),
|
||||
);
|
||||
} on ProfileRefused catch (refused) {
|
||||
emit(
|
||||
PublicProfileStateReady(profile: profile, problem: refused.problem),
|
||||
);
|
||||
} on Object catch (error) {
|
||||
log('Changing the avatar failed', name: 'Profile', error: error);
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: profile,
|
||||
outcome: PublicProfileOutcome.failed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStarted(
|
||||
PublicProfileEventStarted event,
|
||||
Emitter<PublicProfileState> emit,
|
||||
) async {
|
||||
emit(const PublicProfileStateLoading());
|
||||
try {
|
||||
emit(PublicProfileStateReady(profile: await _profiles.mine()));
|
||||
} on Object catch (error) {
|
||||
log('Reading the profile failed', name: 'Profile', error: error);
|
||||
emit(const PublicProfileStateUnreachable());
|
||||
}
|
||||
}
|
||||
|
||||
/// The save asked for while another was on its way, the latest.
|
||||
PublicProfileEventSaved? _waiting;
|
||||
|
||||
/// The handle chosen last, until a save with it went through or was
|
||||
/// refused for it.
|
||||
String? _chosenHandle;
|
||||
|
||||
Future<void> _onSaved(
|
||||
PublicProfileEventSaved event,
|
||||
Emitter<PublicProfileState> emit,
|
||||
) async {
|
||||
if (event.handle case final handle?) {
|
||||
_chosenHandle = handle;
|
||||
}
|
||||
if (state case PublicProfileStateReady(saving: true)) {
|
||||
_waiting = event;
|
||||
return;
|
||||
}
|
||||
PublicProfileEventSaved? next = event;
|
||||
while (next != null) {
|
||||
if (state case PublicProfileStateReady(:final profile, saving: false)) {
|
||||
await _save(next, profile, emit);
|
||||
}
|
||||
next = _waiting;
|
||||
_waiting = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _save(
|
||||
PublicProfileEventSaved event,
|
||||
PublicProfile? profile,
|
||||
Emitter<PublicProfileState> emit,
|
||||
) async {
|
||||
// No profile is made without a handle.
|
||||
final handle = _chosenHandle ?? profile?.handle;
|
||||
if (handle == null) {
|
||||
return;
|
||||
}
|
||||
emit(PublicProfileStateReady(profile: profile, saving: true));
|
||||
try {
|
||||
final saved = await _profiles.save(
|
||||
handle: handle,
|
||||
displayName: event.displayName,
|
||||
bio: event.bio,
|
||||
links: event.links,
|
||||
);
|
||||
if (saved.handle.toLowerCase() == _chosenHandle?.toLowerCase()) {
|
||||
_chosenHandle = null;
|
||||
}
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: saved,
|
||||
outcome: PublicProfileOutcome.saved,
|
||||
),
|
||||
);
|
||||
// A link new or changed is checked by itself.
|
||||
if (saved.links.any(
|
||||
(link) =>
|
||||
!saved.verifiedLinks.contains(link) &&
|
||||
!_checkedLinks.contains(link),
|
||||
)) {
|
||||
add(const PublicProfileEventLinksChecked(asked: false));
|
||||
}
|
||||
} on ProfileRefused catch (refused) {
|
||||
if (handle == _chosenHandle &&
|
||||
ProfileProblem.handleProblems.contains(refused.problem)) {
|
||||
_chosenHandle = null;
|
||||
}
|
||||
emit(PublicProfileStateReady(profile: profile, problem: refused.problem));
|
||||
} on Object catch (error) {
|
||||
log('Saving the profile failed', name: 'Profile', error: error);
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: profile,
|
||||
outcome: PublicProfileOutcome.failed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRemoved(
|
||||
PublicProfileEventRemoved event,
|
||||
Emitter<PublicProfileState> emit,
|
||||
) async {
|
||||
if (state case PublicProfileStateReady(:final profile?, saving: false)) {
|
||||
emit(PublicProfileStateReady(profile: profile, saving: true));
|
||||
try {
|
||||
final left = await _profiles.remove();
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: left,
|
||||
outcome: PublicProfileOutcome.removed,
|
||||
),
|
||||
);
|
||||
} on ProfileRefused catch (refused) {
|
||||
emit(
|
||||
PublicProfileStateReady(profile: profile, problem: refused.problem),
|
||||
);
|
||||
} on Object catch (error) {
|
||||
log('Removing the profile failed', name: 'Profile', error: error);
|
||||
emit(
|
||||
PublicProfileStateReady(
|
||||
profile: profile,
|
||||
outcome: PublicProfileOutcome.failed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What saving or removing came to, said once.
|
||||
enum PublicProfileOutcome() {
|
||||
/// Saved.
|
||||
saved,
|
||||
|
||||
/// Removed.
|
||||
removed,
|
||||
|
||||
/// The instance couldn't be reached; nothing changed.
|
||||
failed,
|
||||
|
||||
/// The links were checked, as asked.
|
||||
linksChecked,
|
||||
}
|
||||
|
||||
/// Where the person's public profile stands.
|
||||
sealed class const PublicProfileState() with Equatable {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// Being read.
|
||||
final class const PublicProfileStateLoading() extends PublicProfileState;
|
||||
|
||||
/// The instance can't be reached.
|
||||
final class const PublicProfileStateUnreachable() extends PublicProfileState;
|
||||
|
||||
/// Read: the [profile] (null while there is none), whether a change is on
|
||||
/// its way, whether its links are being checked, why the last change was
|
||||
/// refused ([problem]), and what the last one came to ([outcome]); the last
|
||||
/// two cleared by the next state.
|
||||
final class const PublicProfileStateReady({
|
||||
final PublicProfile? profile,
|
||||
final bool saving = false,
|
||||
final bool checkingLinks = false,
|
||||
final ProfileProblem? problem,
|
||||
final PublicProfileOutcome? outcome,
|
||||
}) extends PublicProfileState {
|
||||
@override
|
||||
List<Object?> get props => [profile, saving, checkingLinks, problem, outcome];
|
||||
}
|
||||
|
||||
/// What happens to the person's public profile.
|
||||
sealed class const PublicProfileEvent() with Equatable {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// The profile shows: read it.
|
||||
final class const PublicProfileEventStarted() extends PublicProfileEvent;
|
||||
|
||||
/// The person saves their profile as written: a field they left, or a
|
||||
/// [handle] they chose (null keeps the one chosen last, or the profile's).
|
||||
final class const PublicProfileEventSaved({
|
||||
required final String displayName,
|
||||
required final String bio,
|
||||
required final List<String> links,
|
||||
final String? handle,
|
||||
}) extends PublicProfileEvent {
|
||||
@override
|
||||
List<Object?> get props => [handle, displayName, bio, links];
|
||||
}
|
||||
|
||||
/// The person removes their profile.
|
||||
final class const PublicProfileEventRemoved() extends PublicProfileEvent;
|
||||
|
||||
/// The person chose [picture] as their avatar.
|
||||
final class const PublicProfileEventAvatarChosen(final Uint8List picture)
|
||||
extends PublicProfileEvent {
|
||||
@override
|
||||
List<Object?> get props => [picture];
|
||||
}
|
||||
|
||||
/// The person removes their avatar.
|
||||
final class const PublicProfileEventAvatarRemoved() extends PublicProfileEvent;
|
||||
|
||||
/// Check which links' pages link back to the profile page: [asked] by the
|
||||
/// person, or by itself after a save that brought a link not yet checked.
|
||||
final class const PublicProfileEventLinksChecked({final bool asked = true})
|
||||
extends PublicProfileEvent {
|
||||
@override
|
||||
List<Object?> get props => [asked];
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
/// The side of an avatar, in pixels.
|
||||
const avatarSide = 400;
|
||||
|
||||
/// [picture] as an avatar: its middle square, drawn anew at [avatarSide]
|
||||
/// pixels a side (or its own, if smaller), encoded as a PNG. Drawing it
|
||||
/// anew leaves out everything but the pixels: metadata such as where a
|
||||
/// photo was taken stays on the device. Throws for what isn't a picture.
|
||||
Future<Uint8List> avatarPng(Uint8List picture) async {
|
||||
final codec = await ui.instantiateImageCodec(picture);
|
||||
final ui.Image source;
|
||||
try {
|
||||
source = (await codec.getNextFrame()).image;
|
||||
} finally {
|
||||
codec.dispose();
|
||||
}
|
||||
try {
|
||||
final square = math.min(source.width, source.height);
|
||||
final side = math.min(square, avatarSide);
|
||||
final recorder = ui.PictureRecorder();
|
||||
ui.Canvas(recorder).drawImageRect(
|
||||
source,
|
||||
ui.Rect.fromLTWH(
|
||||
(source.width - square) / 2,
|
||||
(source.height - square) / 2,
|
||||
square.toDouble(),
|
||||
square.toDouble(),
|
||||
),
|
||||
ui.Rect.fromLTWH(0, 0, side.toDouble(), side.toDouble()),
|
||||
ui.Paint()..filterQuality = ui.FilterQuality.high,
|
||||
);
|
||||
final drawn = await recorder.endRecording().toImage(side, side);
|
||||
try {
|
||||
final png = await drawn.toByteData(format: ui.ImageByteFormat.png);
|
||||
return png!.buffer.asUint8List(png.offsetInBytes, png.lengthInBytes);
|
||||
} finally {
|
||||
drawn.dispose();
|
||||
}
|
||||
} finally {
|
||||
source.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
|
||||
|
||||
/// The boundary to the person's public profile on the instance. Tests
|
||||
/// replace it with a fake.
|
||||
abstract interface class ProfileGateway() {
|
||||
/// The person's public profile; null while they have none.
|
||||
Future<PublicProfile?> mine();
|
||||
|
||||
/// The public profile of whoever has [handle] on the instance; null
|
||||
/// when nobody has.
|
||||
Future<PublicProfile?> byHandle(String handle);
|
||||
|
||||
/// Saves the person's public profile, making it if they have none.
|
||||
/// Throws [ProfileRefused] when the instance refuses it.
|
||||
Future<PublicProfile> save({
|
||||
required String handle,
|
||||
required String displayName,
|
||||
required String bio,
|
||||
required List<String> links,
|
||||
});
|
||||
|
||||
/// Why [handle] couldn't be the person's, as saving would refuse it; null
|
||||
/// when it can.
|
||||
Future<ProfileProblem?> checkHandle(String handle);
|
||||
|
||||
/// Removes what the person's public profile shows, making it private
|
||||
/// again with its handle (ADR 0020, D27); returns what is left, null while
|
||||
/// they have none. Throws [ProfileRefused] once they have published.
|
||||
Future<PublicProfile?> remove();
|
||||
|
||||
/// Shows [png] as the person's avatar; returns the profile with it.
|
||||
/// Throws [ProfileRefused] for anything but a PNG of at most 1 MB.
|
||||
Future<PublicProfile> setAvatar(Uint8List png);
|
||||
|
||||
/// Shows no avatar any more; returns the profile without it.
|
||||
Future<PublicProfile> removeAvatar();
|
||||
|
||||
/// Has the instance check which of the person's links' pages link back to
|
||||
/// their profile page; returns the profile with the answer. Throws
|
||||
/// [ProfileRefused] when it was asked too often.
|
||||
Future<PublicProfile> checkLinks();
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/data/avatar_image.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
|
||||
|
||||
/// The person's public profile (ADR 0005, rule 2). It lives on the
|
||||
/// instance, not in the person's synced data, so it needs the instance to
|
||||
/// be reachable. A picture becomes an avatar through [_drawAvatar]
|
||||
/// ([avatarPng] unless a test gives another).
|
||||
class ProfileRepository(
|
||||
final ProfileGateway _gateway, {
|
||||
final Future<Uint8List> Function(Uint8List picture) _drawAvatar = avatarPng,
|
||||
}) {
|
||||
/// The person's public profile; null while they have none.
|
||||
Future<PublicProfile?> mine() => _gateway.mine();
|
||||
|
||||
/// The public profile of whoever has [handle] (without its `@`) on the
|
||||
/// instance; null when nobody has.
|
||||
Future<PublicProfile?> byHandle(String handle) => _gateway.byHandle(handle);
|
||||
|
||||
/// Saves the person's public profile, blank links left out. Throws
|
||||
/// [ProfileRefused] when the instance refuses it.
|
||||
Future<PublicProfile> save({
|
||||
required String handle,
|
||||
required String displayName,
|
||||
required String bio,
|
||||
required List<String> links,
|
||||
}) => _gateway.save(
|
||||
handle: handle.trim(),
|
||||
displayName: displayName.trim(),
|
||||
bio: bio.trim(),
|
||||
links: [
|
||||
for (final link in links)
|
||||
if (link.trim().isNotEmpty) link.trim(),
|
||||
],
|
||||
);
|
||||
|
||||
/// Why [handle] couldn't be the person's, as saving would refuse it: not
|
||||
/// a handle, reserved, someone else's, or a new one once they have
|
||||
/// published; null when it can, as their own can (decided 2026-10-12, to
|
||||
/// say so while it is typed).
|
||||
Future<ProfileProblem?> checkHandle(String handle) =>
|
||||
_gateway.checkHandle(handle.trim());
|
||||
|
||||
/// Removes what the person's public profile shows, making it private
|
||||
/// again with its handle (ADR 0020, D27); returns what is left, null while
|
||||
/// they have none. Throws [ProfileRefused] once they have published.
|
||||
Future<PublicProfile?> remove() => _gateway.remove();
|
||||
|
||||
/// Shows [picture] (a PNG, JPEG, WebP or GIF) as the person's avatar,
|
||||
/// drawn anew as a square PNG ([avatarPng]): metadata such as where a
|
||||
/// photo was taken never leaves the device. Throws [ProfileRefused] for
|
||||
/// a picture that can't be read.
|
||||
Future<PublicProfile> setAvatar(Uint8List picture) async {
|
||||
final Uint8List png;
|
||||
try {
|
||||
png = await _drawAvatar(picture);
|
||||
} on Object {
|
||||
throw ProfileRefused(ProfileProblem.avatarInvalid);
|
||||
}
|
||||
return await _gateway.setAvatar(png);
|
||||
}
|
||||
|
||||
/// Shows no avatar any more.
|
||||
Future<PublicProfile> removeAvatar() => _gateway.removeAvatar();
|
||||
|
||||
/// Has the instance check which of the person's links' pages link back to
|
||||
/// their profile page with `rel="me"` (ADR 0011). Throws [ProfileRefused]
|
||||
/// when it was asked too often.
|
||||
Future<PublicProfile> checkLinks() => _gateway.checkLinks();
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as base;
|
||||
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
|
||||
|
||||
/// [ProfileGateway] asking the instance through the Serverpod client: the
|
||||
/// profile endpoints are halkyon_base's, as the instance's register keeps
|
||||
/// people's profiles (ADR 0016).
|
||||
class ServerpodProfileGateway(final protocol.Client _client)
|
||||
implements ProfileGateway {
|
||||
/// The caller's own profile's endpoint.
|
||||
base.EndpointProfile get _profiles => _client.modules.base.profile;
|
||||
|
||||
@override
|
||||
Future<PublicProfile?> mine() async => switch (await _profiles.mine()) {
|
||||
final profile? => _profile(profile),
|
||||
null => null,
|
||||
};
|
||||
|
||||
@override
|
||||
Future<PublicProfile> save({
|
||||
required String handle,
|
||||
required String displayName,
|
||||
required String bio,
|
||||
required List<String> links,
|
||||
}) => _refusing(
|
||||
() async => _profile(
|
||||
await _profiles.save(handle, displayName, bio, links),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<PublicProfile?> byHandle(String handle) async {
|
||||
try {
|
||||
return _profile(await _client.modules.base.profiles.byHandle(handle));
|
||||
} on base.ProfileNotFoundException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ProfileProblem?> checkHandle(String handle) async =>
|
||||
switch (await _profiles.checkHandle(handle)) {
|
||||
null => null,
|
||||
// A reason this app doesn't know reads as an invalid handle.
|
||||
final reason =>
|
||||
ProfileProblem.values.asNameMap()[reason.name] ??
|
||||
ProfileProblem.handleInvalid,
|
||||
};
|
||||
|
||||
@override
|
||||
Future<PublicProfile?> remove() => _refusing(
|
||||
() async => switch (await _profiles.remove()) {
|
||||
final profile? => _profile(profile),
|
||||
null => null,
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Future<PublicProfile> setAvatar(Uint8List png) => _refusing(
|
||||
() async => _profile(
|
||||
await _profiles.setAvatar(ByteData.sublistView(png)),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<PublicProfile> removeAvatar() =>
|
||||
_refusing(() async => _profile(await _profiles.removeAvatar()));
|
||||
|
||||
@override
|
||||
Future<PublicProfile> checkLinks() =>
|
||||
_refusing(() async => _profile(await _profiles.checkLinks()));
|
||||
|
||||
/// [call], its refusal as a [ProfileRefused]; a reason this app doesn't
|
||||
/// know reads as an invalid handle.
|
||||
Future<T> _refusing<T>(Future<T> Function() call) async {
|
||||
try {
|
||||
return await call();
|
||||
} on base.ProfileRefusedException catch (error) {
|
||||
throw ProfileRefused(
|
||||
ProfileProblem.values.asNameMap()[error.reason.name] ??
|
||||
ProfileProblem.handleInvalid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PublicProfile _profile(base.Profile profile) => PublicProfile(
|
||||
handle: profile.handle,
|
||||
address: profile.address,
|
||||
displayName: profile.displayName,
|
||||
bio: profile.bio,
|
||||
links: profile.links,
|
||||
verifiedLinks: profile.verifiedLinks ?? const [],
|
||||
pageUrl: profile.pageUrl,
|
||||
avatarUrl: profile.avatarUrl,
|
||||
handleFixed: profile.handleFixed,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A person's public profile (ADR 0005, rule 2): the handle others find
|
||||
/// them by, at the instance's account domain ([address],
|
||||
/// `raisondetredev@halkyon.eu`), a display name, a bio, links and an
|
||||
/// avatar.
|
||||
final class const PublicProfile({
|
||||
/// The handle as its person wrote it.
|
||||
required final String handle,
|
||||
|
||||
/// The handle with the account domain.
|
||||
required final String address,
|
||||
|
||||
/// The name shown beside the handle; empty for none.
|
||||
final String displayName = '',
|
||||
|
||||
/// A few words about the person; empty for none.
|
||||
final String bio = '',
|
||||
|
||||
/// Pages about the person, in their order.
|
||||
final List<String> links = const [],
|
||||
|
||||
/// Those of [links] whose pages link back to [pageUrl] with `rel="me"`,
|
||||
/// as the instance last checked (ADR 0011).
|
||||
final List<String> verifiedLinks = const [],
|
||||
|
||||
/// The profile's page on the web (`https://halkyon.eu/@raisondetredev`),
|
||||
/// which the person's other pages link back to; null where the instance
|
||||
/// has none.
|
||||
final String? pageUrl,
|
||||
|
||||
/// Where their avatar is, a PNG; null for none.
|
||||
final String? avatarUrl,
|
||||
|
||||
/// Whether the handle is fixed: its person has published.
|
||||
final bool handleFixed = false,
|
||||
}) with Equatable {
|
||||
/// This profile with [verified] as its links checked, those it still has.
|
||||
PublicProfile withVerifiedLinks(List<String> verified) => PublicProfile(
|
||||
handle: handle,
|
||||
address: address,
|
||||
displayName: displayName,
|
||||
bio: bio,
|
||||
links: links,
|
||||
verifiedLinks: [
|
||||
for (final link in verified)
|
||||
if (links.contains(link)) link,
|
||||
],
|
||||
pageUrl: pageUrl,
|
||||
avatarUrl: avatarUrl,
|
||||
handleFixed: handleFixed,
|
||||
);
|
||||
|
||||
/// The most links a profile holds, as the instance allows.
|
||||
static const maxLinks = 4;
|
||||
|
||||
/// The longest a display name may be, as the instance allows.
|
||||
static const maxDisplayNameLength = 30;
|
||||
|
||||
/// The longest a handle may be (Mastodon's rule, as the instance's).
|
||||
static const maxHandleLength = 30;
|
||||
|
||||
/// The longest a bio may be, as the instance allows.
|
||||
static const maxBioLength = 500;
|
||||
|
||||
/// The longest a link may be, as the instance allows.
|
||||
static const maxLinkLength = 2000;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
handle,
|
||||
address,
|
||||
displayName,
|
||||
bio,
|
||||
links,
|
||||
verifiedLinks,
|
||||
pageUrl,
|
||||
avatarUrl,
|
||||
handleFixed,
|
||||
];
|
||||
}
|
||||
|
||||
/// Why the instance refused to save or remove a profile.
|
||||
enum ProfileProblem() {
|
||||
/// The handle isn't 1 to 30 letters, digits or underscores.
|
||||
handleInvalid,
|
||||
|
||||
/// The handle is kept for the instance.
|
||||
handleReserved,
|
||||
|
||||
/// Someone else has the handle.
|
||||
handleTaken,
|
||||
|
||||
/// The person has published: the handle is fixed, the profile stays.
|
||||
published,
|
||||
|
||||
/// The display name is too long.
|
||||
displayNameTooLong,
|
||||
|
||||
/// The bio is too long.
|
||||
bioTooLong,
|
||||
|
||||
/// Too many links.
|
||||
tooManyLinks,
|
||||
|
||||
/// A link isn't an absolute HTTP(S) URL.
|
||||
linkInvalid,
|
||||
|
||||
/// The picture can't be read, or isn't a PNG of at most 1 MB.
|
||||
avatarInvalid,
|
||||
|
||||
/// The links were checked too often in the last hour.
|
||||
tooManyLinkChecks,
|
||||
|
||||
/// Too many handles were asked about in the last hour.
|
||||
tooManyHandleChecks;
|
||||
|
||||
/// The problems with a handle.
|
||||
static const handleProblems = {
|
||||
handleInvalid,
|
||||
handleReserved,
|
||||
handleTaken,
|
||||
published,
|
||||
tooManyHandleChecks,
|
||||
};
|
||||
}
|
||||
|
||||
/// The instance refused to save or remove a profile, for [problem];
|
||||
/// nothing changed.
|
||||
final class ProfileRefused(final ProfileProblem problem) implements Exception {
|
||||
@override
|
||||
String toString() => 'ProfileRefused($problem)';
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// Shows the public profile of whoever has [handle] (without its `@`) on
|
||||
/// the instance, as a studyset names its author or licence holder
|
||||
/// (decided 2026-10-14): their avatar, name and address, what they say
|
||||
/// about themselves and their links; that nobody has it, when nobody has.
|
||||
Future<void> showProfileOf(BuildContext context, String handle) {
|
||||
final profiles = PersonDependencies.of(context).profile;
|
||||
return showAdaptiveSheet<void>(
|
||||
context,
|
||||
builder: (context) => _ProfileSheet(profile: profiles.byHandle(handle)),
|
||||
);
|
||||
}
|
||||
|
||||
class const _ProfileSheet({required final Future<PublicProfile?> profile})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final muted = theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
);
|
||||
return FutureBuilder(
|
||||
future: profile,
|
||||
builder: (context, snapshot) {
|
||||
final found = snapshot.data;
|
||||
return SheetScaffold(
|
||||
title: found == null
|
||||
? l10n.profileSheetTitle
|
||||
: found.displayName.isEmpty
|
||||
? '@${found.handle}'
|
||||
: found.displayName,
|
||||
body: switch (snapshot.connectionState) {
|
||||
ConnectionState.done when found == null => Text(
|
||||
snapshot.hasError
|
||||
? l10n.profileSheetUnreachable
|
||||
: l10n.profileSheetNotFound,
|
||||
style: muted,
|
||||
),
|
||||
ConnectionState.done => _Profile(profile: found!),
|
||||
_ => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
child: Center(child: AppLoadingIndicator()),
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class const _Profile({required final PublicProfile profile})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final muted = theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
foregroundImage: switch (profile.avatarUrl) {
|
||||
final url? => NetworkImage(url),
|
||||
null => null,
|
||||
},
|
||||
child: const Icon(LucideIcons.user),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'@${profile.address}',
|
||||
style: muted,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (profile.bio.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(profile.bio),
|
||||
],
|
||||
if (profile.links.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
for (final link in profile.links)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
leading: const Icon(LucideIcons.link),
|
||||
title: Text(
|
||||
link.replaceFirst(RegExp('^https?://'), ''),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// Its page links back to this profile's (ADR 0011).
|
||||
trailing: profile.verifiedLinks.contains(link)
|
||||
? Tooltip(
|
||||
message: AppLocalizations.of(
|
||||
context,
|
||||
).profileSheetLinkVerified,
|
||||
child: Icon(
|
||||
LucideIcons.badgeCheck,
|
||||
color: theme.colorScheme.primary,
|
||||
semanticLabel: AppLocalizations.of(
|
||||
context,
|
||||
).profileSheetLinkVerified,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () => unawaited(
|
||||
launchUrl(
|
||||
Uri.parse(link.contains('://') ? link : 'https://$link'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue