diff --git a/android-vault b/android-vault
index 15225ede6c..2f81632319 160000
--- a/android-vault
+++ b/android-vault
@@ -1 +1 @@
-Subproject commit 15225ede6c44da8265e5cdaea34d49dbc47cb8f5
+Subproject commit 2f816323198928844c274ede6692e0fd8430d4d1
diff --git a/apps/build.gradle b/apps/build.gradle
index 8d5d077915..c9cbf4d9db 100644
--- a/apps/build.gradle
+++ b/apps/build.gradle
@@ -33,7 +33,6 @@ buildscript {
classpath Plugins.KOTLIN
classpath Plugins.FIREBASE_CRASHLYTICS
if (project.coverageEnabled) { classpath Plugins.JACOCO_ANDROID }
- classpath Plugins.SQLDELIGHT
classpath Plugins.HILT
classpath Plugins.HEAP
}
diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle
index 5fd86f30e5..0190a20214 100644
--- a/apps/flutter_parent/android/app/build.gradle
+++ b/apps/flutter_parent/android/app/build.gradle
@@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
- compileSdk 33
+ compileSdk 34
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -46,7 +46,7 @@ android {
defaultConfig {
applicationId "com.instructure.parentapp"
minSdkVersion 26
- targetSdk 33
+ targetSdk 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart
index 86768767b3..b7f03e6be3 100644
--- a/apps/flutter_parent/lib/l10n/app_localizations.dart
+++ b/apps/flutter_parent/lib/l10n/app_localizations.dart
@@ -1715,4 +1715,14 @@ class AppLocalizations {
String get needToEnablePermission =>
Intl.message('You need to enable exact alarm permission for this action', desc: 'Error message when the user tries to set a reminder without the permission');
+
+ String get submissionAndRubric => Intl.message(
+ 'Submission & Rubric',
+ desc: 'Button text for Submission and Rubric on Assignment Details Screen'
+ );
+
+ String get submission => Intl.message(
+ 'Submission',
+ desc: 'Title for WebView screen when opening submission'
+ );
}
diff --git a/apps/flutter_parent/lib/l10n/res/intl_ga.arb b/apps/flutter_parent/lib/l10n/res/intl_ga.arb
new file mode 100644
index 0000000000..bea1391b7a
--- /dev/null
+++ b/apps/flutter_parent/lib/l10n/res/intl_ga.arb
@@ -0,0 +1,2753 @@
+{
+ "@@last_modified": "2023-08-25T11:04:20.901151",
+ "alertsLabel": "Foláirimh",
+ "@alertsLabel": {
+ "description": "The label for the Alerts tab",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "calendarLabel": "Féilire",
+ "@calendarLabel": {
+ "description": "The label for the Calendar tab",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "coursesLabel": "Cúrsaí",
+ "@coursesLabel": {
+ "description": "The label for the Courses tab",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Students": "Níl Mic Léinn ann",
+ "@No Students": {
+ "description": "Text for when an observer has no students they are observing",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tap to show student selector": "Tapáil chun roghnóir an mhic léinn a thaispeáint",
+ "@Tap to show student selector": {
+ "description": "Semantics label for the area that will show the student selector when tapped",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tap to pair with a new student": "Tapáil chun péireáil le mac léinn nua",
+ "@Tap to pair with a new student": {
+ "description": "Semantics label for the add student button in the student selector",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tap to select this student": "Tapáil chun an mac léinn seo a roghnú",
+ "@Tap to select this student": {
+ "description": "Semantics label on individual students in the student switcher",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Manage Students": "Bainistigh Mic Léinn",
+ "@Manage Students": {
+ "description": "Label text for the Manage Students nav drawer button as well as the title for the Manage Students screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Help": "Cabhair",
+ "@Help": {
+ "description": "Label text for the help nav drawer button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Log Out": "Logáil Amach",
+ "@Log Out": {
+ "description": "Label text for the Log Out nav drawer button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Switch Users": "Malartaigh Úsáideoirí",
+ "@Switch Users": {
+ "description": "Label text for the Switch Users nav drawer button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "appVersion": "v. {version}",
+ "@appVersion": {
+ "description": "App version shown in the navigation drawer",
+ "type": "text",
+ "placeholders_order": [
+ "version"
+ ],
+ "placeholders": {
+ "version": {}
+ }
+ },
+ "Are you sure you want to log out?": "An bhfuil tú cinnte gur mhaith leat logáil amach?",
+ "@Are you sure you want to log out?": {
+ "description": "Confirmation message displayed when the user tries to log out",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Calendars": "Féilirí",
+ "@Calendars": {
+ "description": "Label for button that lets users select which calendars to display",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "nextMonth": "An mhí seo chugainn: {month}",
+ "@nextMonth": {
+ "description": "Label for the button that switches the calendar to the next month",
+ "type": "text",
+ "placeholders_order": [
+ "month"
+ ],
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "previousMonth": "An mhí roimhe sin: {month}",
+ "@previousMonth": {
+ "description": "Label for the button that switches the calendar to the previous month",
+ "type": "text",
+ "placeholders_order": [
+ "month"
+ ],
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "nextWeek": "An tseachtain seo chugainn ag tosnú {date}",
+ "@nextWeek": {
+ "description": "Label for the button that switches the calendar to the next week",
+ "type": "text",
+ "placeholders_order": [
+ "date"
+ ],
+ "placeholders": {
+ "date": {}
+ }
+ },
+ "previousWeek": "An tseachtain roimhe ag tosnú {date}",
+ "@previousWeek": {
+ "description": "Label for the button that switches the calendar to the previous week",
+ "type": "text",
+ "placeholders_order": [
+ "date"
+ ],
+ "placeholders": {
+ "date": {}
+ }
+ },
+ "selectedMonthLabel": "Mí {month}",
+ "@selectedMonthLabel": {
+ "description": "Accessibility label for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders_order": [
+ "month"
+ ],
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "expand": "leathnaigh",
+ "@expand": {
+ "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "collapse": "leacaigh",
+ "@collapse": {
+ "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "pointsPossible": "{points} pointe féideartha",
+ "@pointsPossible": {
+ "description": "Screen reader label used for the points possible for an assignment, quiz, etc.",
+ "type": "text",
+ "placeholders_order": [
+ "points"
+ ],
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} imeacht}other{{date}, {eventCount} imeacht}}",
+ "@calendarDaySemanticsLabel": {
+ "description": "Screen reader label used for calendar day, reads the date and count of events",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "eventCount"
+ ],
+ "placeholders": {
+ "date": {},
+ "eventCount": {}
+ }
+ },
+ "No Events Today!": "Gan Imeacht inniu!",
+ "@No Events Today!": {
+ "description": "Title displayed when there are no calendar events for the current day",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "It looks like a great day to rest, relax, and recharge.": "Breathnaíonn sé cosúil le lá iontach chun sos a ghlacadh, scíth a ligean, agus d'anáil a tharraingt.",
+ "@It looks like a great day to rest, relax, and recharge.": {
+ "description": "Message displayed when there are no calendar events for the current day",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your student's calendar": "Tharla earráid agus féilire do mhic léinn á lódáil",
+ "@There was an error loading your student's calendar": {
+ "description": "Message displayed when calendar events could not be loaded for the current student",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tap to favorite the courses you want to see on the Calendar. Select up to 10.": "Tapáil chun na cúrsaí is fearr leat a fheiceáil ar an bhFéilire. Roghnaigh suas le 10 gcinn.",
+ "@Tap to favorite the courses you want to see on the Calendar. Select up to 10.": {
+ "description": "Description text on calendar filter screen.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You may only choose 10 calendars to display": "Ní féidir leat ach 10 bhféilire a roghnú lena thaispeáint",
+ "@You may only choose 10 calendars to display": {
+ "description": "Error text when trying to select more than 10 calendars",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You must select at least one calendar to display": "Ní mór duit féilire amháin ar a laghad a roghnú lena thaispeáint",
+ "@You must select at least one calendar to display": {
+ "description": "Error text when trying to de-select all calendars",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Planner Note": "Nóta Pleanálaí",
+ "@Planner Note": {
+ "description": "Label used for notes in the planner",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Go to today": "Téigh go dtí inniu",
+ "@Go to today": {
+ "description": "Accessibility label used for the today button in the planner",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Previous Logins": "Logáil Isteach Roimhe Seo",
+ "@Previous Logins": {
+ "description": "Label for the list of previous user logins",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "canvasLogoLabel": "Lógó Canvas",
+ "@canvasLogoLabel": {
+ "description": "The semantics label for the Canvas logo",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "findSchool": "Aimsigh Scoil",
+ "@findSchool": {
+ "description": "Text for the find-my-school button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "findAnotherSchool": "Aimsigh scoil eile",
+ "@findAnotherSchool": {
+ "description": "Text for the find-another-school button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "domainSearchInputHint": "Cuir isteach ainm scoile nó ceantar…",
+ "@domainSearchInputHint": {
+ "description": "Input hint for the text box on the domain search screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "noDomainResults": "Ní féidir scoileanna a mheaitseálann \"{query}\" a aimsiú",
+ "@noDomainResults": {
+ "description": "Message shown to users when the domain search query did not return any results",
+ "type": "text",
+ "placeholders_order": [
+ "query"
+ ],
+ "placeholders": {
+ "query": {}
+ }
+ },
+ "domainSearchHelpLabel": "Conas a aimsím mo scoil nó mo cheantar?",
+ "@domainSearchHelpLabel": {
+ "description": "Label for the help button on the domain search screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "canvasGuides": "Treoracha Canvas",
+ "@canvasGuides": {
+ "description": "Proper name for the Canvas Guides. This will be used in the domainSearchHelpBody text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "canvasSupport": "Tacaíocht Canvas",
+ "@canvasSupport": {
+ "description": "Proper name for Canvas Support. This will be used in the domainSearchHelpBody text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "domainSearchHelpBody": "Déan iarracht ainm na scoile nó an cheantair a bhfuil tú ag iarraidh rochtain a fháil air a chuardach, ar nós “Smith Private School” nó “Smith County Schools”. Is féidir leat fearann Canvas a chur isteach go díreach freisin, amhail “smith.instructure.com.” \n\nChun tuilleadh faisnéise a fháil maidir le cuntas Canvas d’institiúide a aimsiú, is féidir leat cuairt a thabhairt ar an {canvasGuides}, déan teagmháil le {canvasSupport}, nó déan teagmháil le do scoil chun cúnamh a fháil.",
+ "@domainSearchHelpBody": {
+ "description": "The body text shown in the help dialog on the domain search screen",
+ "type": "text",
+ "placeholders_order": [
+ "canvasGuides",
+ "canvasSupport"
+ ],
+ "placeholders": {
+ "canvasGuides": {},
+ "canvasSupport": {}
+ }
+ },
+ "Uh oh!": "Uh ó!",
+ "@Uh oh!": {
+ "description": "Title of the screen that shows when a crash has occurred",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Níl muid cinnte cad a tharla, ach ní raibh sé go maith. Déan teagmháil linn má leanann sé seo ag tarlú.",
+ "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": {
+ "description": "Message shown when a crash has occurred",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Contact Support": "Tacaíocht Teagmhála",
+ "@Contact Support": {
+ "description": "Label for the button that allows users to contact support after a crash has occurred",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "View error details": "Féach ar shonraí earráide",
+ "@View error details": {
+ "description": "Label for the button that allowed users to view crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Restart app": "Atosaigh an aip",
+ "@Restart app": {
+ "description": "Label for the button that will restart the entire application",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Application version": "Leagan feidhmchláir",
+ "@Application version": {
+ "description": "Label for the application version displayed in the crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Device model": "Múnla gléas",
+ "@Device model": {
+ "description": "Label for the device model displayed in the crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Android OS version": "Android leagan OS",
+ "@Android OS version": {
+ "description": "Label for the Android operating system version displayed in the crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Full error message": "Teachtaireacht earráide iomlán",
+ "@Full error message": {
+ "description": "Label for the full error message displayed in the crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Inbox": "Bosca Isteach",
+ "@Inbox": {
+ "description": "Title for the Inbox screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your inbox messages.": "Tharla earráid agus do theachtaireachtaí sa bhosca isteach á lódáil.",
+ "@There was an error loading your inbox messages.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Subject": "Níl aon Ábhar",
+ "@No Subject": {
+ "description": "Title used for inbox messages that have no subject",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unable to fetch courses. Please check your connection and try again.": "Ní féidir cúrsaí a fháil. Seiceáil do cheangal agus bain triail eile as.",
+ "@Unable to fetch courses. Please check your connection and try again.": {
+ "description": "Message shown when an error occured while loading courses",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Choose a course to message": "Roghnaigh cúrsa chun teachtaireacht a dhéanamh",
+ "@Choose a course to message": {
+ "description": "Header in the course list shown when the user is choosing which course to associate with a new message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Inbox Zero": "Bosca Isteach Nialais",
+ "@Inbox Zero": {
+ "description": "Title of the message shown when there are no inbox messages",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You’re all caught up!": "Tá tú suas chun dáta ar fad!",
+ "@You’re all caught up!": {
+ "description": "Subtitle of the message shown when there are no inbox messages",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading recipients for this course": "Tharla earráid agus faighteoirí á lódáil don chúrsa seo",
+ "@There was an error loading recipients for this course": {
+ "description": "Message shown when attempting to create a new message but the recipients list failed to load",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unable to send message. Check your connection and try again.": "Ní féidir teachtaireacht a sheoladh. Seiceáil do cheangal agus bain triail eile as.",
+ "@Unable to send message. Check your connection and try again.": {
+ "description": "Message show when there was an error creating or sending a new message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unsaved changes": "Athruithe nach bhfuil sábháilte",
+ "@Unsaved changes": {
+ "description": "Title of the dialog shown when the user tries to leave with unsaved changes",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Are you sure you wish to close this page? Your unsent message will be lost.": "An bhfuil tú cinnte gur mian leat an leathanach seo a dhúnadh? Caillfear do theachtaireacht nár seoladh.",
+ "@Are you sure you wish to close this page? Your unsent message will be lost.": {
+ "description": "Body text of the dialog shown when the user tries leave with unsaved changes",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "New message": "Teachtaireacht nua",
+ "@New message": {
+ "description": "Title of the new-message screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Add attachment": "Cuir ceangaltán leis",
+ "@Add attachment": {
+ "description": "Tooltip for the add-attachment button in the new-message screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Send message": "Seol teachtaireacht",
+ "@Send message": {
+ "description": "Tooltip for the send-message button in the new-message screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Select recipients": "Roghnaigh faighteoirí",
+ "@Select recipients": {
+ "description": "Tooltip for the button that allows users to select message recipients",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No recipients selected": "Níor roghnaíodh aon fhaighteoir",
+ "@No recipients selected": {
+ "description": "Hint displayed when the user has not selected any message recipients",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Message subject": "Ábhar na teachtaireachta",
+ "@Message subject": {
+ "description": "Hint text displayed in the input field for the message subject",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Message": "Teachtaireacht",
+ "@Message": {
+ "description": "Hint text displayed in the input field for the message body",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Recipients": "Faighteoirí",
+ "@Recipients": {
+ "description": "Label for message recipients",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "plusRecipientCount": "+{count}",
+ "@plusRecipientCount": {
+ "description": "Shows the number of recipients that are selected but not displayed on screen.",
+ "type": "text",
+ "placeholders_order": [
+ "count"
+ ],
+ "placeholders": {
+ "count": {
+ "example": 5
+ }
+ }
+ },
+ "Failed. Tap for options.": "Theip air. Tapáil le haghaidh roghanna.",
+ "@Failed. Tap for options.": {
+ "description": "Short message shown on a message attachment when uploading has failed",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "courseForWhom": "le haghaidh {studentShortName}",
+ "@courseForWhom": {
+ "description": "Describes for whom a course is for (i.e. for Bill)",
+ "type": "text",
+ "placeholders_order": [
+ "studentShortName"
+ ],
+ "placeholders": {
+ "studentShortName": {}
+ }
+ },
+ "messageLinkPostscript": "Maidir le: {studentName}, {linkUrl}",
+ "@messageLinkPostscript": {
+ "description": "A postscript appended to new messages that clarifies which student is the subject of the message and also includes a URL for the related Canvas component (course, assignment, event, etc).",
+ "type": "text",
+ "placeholders_order": [
+ "studentName",
+ "linkUrl"
+ ],
+ "placeholders": {
+ "studentName": {},
+ "linkUrl": {}
+ }
+ },
+ "There was an error loading this conversation": "Tharla earráid agus an comhrá seo á lódáil",
+ "@There was an error loading this conversation": {
+ "description": "Message shown when a conversation fails to load",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Reply": "Freagair",
+ "@Reply": {
+ "description": "Button label for replying to a conversation",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Reply All": "Freagair Do Chách",
+ "@Reply All": {
+ "description": "Button label for replying to all conversation participants",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unknown User": "Úsáideoir Anaithnid",
+ "@Unknown User": {
+ "description": "Label used where the user name is not known",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "me": "mé",
+ "@me": {
+ "description": "First-person pronoun (i.e. 'me') that will be used in message author info, e.g. 'Me to 4 others' or 'Jon Snow to me'",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "authorToRecipient": "{authorName} go {recipientName}",
+ "@authorToRecipient": {
+ "description": "Author info for a single-recipient message; includes both the author name and the recipient name.",
+ "type": "text",
+ "placeholders_order": [
+ "authorName",
+ "recipientName"
+ ],
+ "placeholders": {
+ "authorName": {},
+ "recipientName": {}
+ }
+ },
+ "authorToNOthers": "{howMany,plural, =1{{authorName} go 1 eile}other{{authorName} go {howMany} daoine eile}}",
+ "@authorToNOthers": {
+ "description": "Author info for a mutli-recipient message; includes the author name and the number of recipients",
+ "type": "text",
+ "placeholders_order": [
+ "authorName",
+ "howMany"
+ ],
+ "placeholders": {
+ "authorName": {},
+ "howMany": {}
+ }
+ },
+ "authorToRecipientAndNOthers": "{howMany,plural, =1{{authorName} go {recipientName} & 1 eile}other{{authorName} go {recipientName} & {howMany} daoine eile}}",
+ "@authorToRecipientAndNOthers": {
+ "description": "Author info for a multi-recipient message; includes the author name, one recipient name, and the number of other recipients",
+ "type": "text",
+ "placeholders_order": [
+ "authorName",
+ "recipientName",
+ "howMany"
+ ],
+ "placeholders": {
+ "authorName": {},
+ "recipientName": {},
+ "howMany": {}
+ }
+ },
+ "Download": "Íoslódáil",
+ "@Download": {
+ "description": "Label for the button that will begin downloading a file",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Open with another app": "Oscail le aip eile",
+ "@Open with another app": {
+ "description": "Label for the button that will allow users to open a file with another app",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There are no installed applications that can open this file": "Níl aon fheidhmchláir suiteáilte ar féidir an comhad seo a oscailt",
+ "@There are no installed applications that can open this file": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unsupported File": "Comhad nach dtacaítear leis",
+ "@Unsupported File": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "This file is unsupported and can’t be viewed through the app": "Ní thacaítear leis an gcomhad seo agus ní féidir é a fheiceáil tríd an aip",
+ "@This file is unsupported and can’t be viewed through the app": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unable to play this media file": "Ní féidir an comhad meáin seo a sheinm",
+ "@Unable to play this media file": {
+ "description": "Message shown when audio or video media could not be played",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unable to load this image": "Ní féidir an íomhá seo a lódáil",
+ "@Unable to load this image": {
+ "description": "Message shown when an image file could not be loaded or displayed",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading this file": "Tharla earráid agus an comhad seo á lódáil",
+ "@There was an error loading this file": {
+ "description": "Message shown when a file could not be loaded or displayed",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Courses": "Níl aon Chúrsaí",
+ "@No Courses": {
+ "description": "Title for having no courses",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Your student’s courses might not be published yet.": "Seans nach bhfuil cúrsaí do mhic léinn foilsithe fós.",
+ "@Your student’s courses might not be published yet.": {
+ "description": "Message for having no courses",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your student’s courses.": "Tharla earráid agus cúrsaí do mhic léinn á lódáil.",
+ "@There was an error loading your student’s courses.": {
+ "description": "Message displayed when the list of student courses could not be loaded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Grade": "Níl Marc Ann",
+ "@No Grade": {
+ "description": "Message shown when there is currently no grade available for a course",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Filter by": "Scag de réir",
+ "@Filter by": {
+ "description": "Title for list of terms to filter grades by",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Grades": "Marcanna",
+ "@Grades": {
+ "description": "Label for the \"Grades\" tab in course details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Syllabus": "Siollabas",
+ "@Syllabus": {
+ "description": "Label for the \"Syllabus\" tab in course details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Front Page": "Leathanach Tosaigh",
+ "@Front Page": {
+ "description": "Label for the \"Front Page\" tab in course details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Summary": "Achoimre",
+ "@Summary": {
+ "description": "Label for the \"Summary\" tab in course details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Send a message about this course": "Seol teachtaireacht faoin gcúrsa seo",
+ "@Send a message about this course": {
+ "description": "Accessibility hint for the course messaage floating action button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Total Grade": "Marc Iomlán",
+ "@Total Grade": {
+ "description": "Label for the total grade in the course",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Graded": "Marcáilte",
+ "@Graded": {
+ "description": "Label for assignments that have been graded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Submitted": "Curtha isteach",
+ "@Submitted": {
+ "description": "Label for assignments that have been submitted",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Not Submitted": "Níl sé Curtha isteach",
+ "@Not Submitted": {
+ "description": "Label for assignments that have not been submitted",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Late": "Mall",
+ "@Late": {
+ "description": "Label for assignments that have been marked late or submitted late",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Missing": "Ar Iarraidh",
+ "@Missing": {
+ "description": "Label for assignments that have been marked missing or are not submitted and past the due date",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "-": "-",
+ "@-": {
+ "description": "Value representing no score for student submission",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "All Grading Periods": "Gach Tréimhse Mharcála",
+ "@All Grading Periods": {
+ "description": "Label for selecting all grading periods",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Assignments": "Níl aon Tascanna ann",
+ "@No Assignments": {
+ "description": "Title for the no assignments message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "It looks like assignments haven't been created in this space yet.": "Tá an chuma ar an scéal nár cruthaíodh tascanna sa spás seo go fóill.",
+ "@It looks like assignments haven't been created in this space yet.": {
+ "description": "Message for no assignments",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading the summary details for this course.": "Tharla earráid agus na sonraí achoimre don chúrsa seo á lódáil.",
+ "@There was an error loading the summary details for this course.": {
+ "description": "Message shown when the course summary could not be loaded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Summary": "Níl Achoimre Ann",
+ "@No Summary": {
+ "description": "Title displayed when there are no items in the course summary",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "This course does not have any assignments or calendar events yet.": "Níl aon tascanna ná imeachtaí féilire ag an gcúrsa seo fós.",
+ "@This course does not have any assignments or calendar events yet.": {
+ "description": "Message displayed when there are no items in the course summary",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "gradeFormatScoreOutOfPointsPossible": "{score} / {pointsPossible}",
+ "@gradeFormatScoreOutOfPointsPossible": {
+ "description": "Formatted string for a student score out of the points possible",
+ "type": "text",
+ "placeholders_order": [
+ "score",
+ "pointsPossible"
+ ],
+ "placeholders": {
+ "score": {},
+ "pointsPossible": {}
+ }
+ },
+ "contentDescriptionScoreOutOfPointsPossible": "{score} as {pointsPossible} pointí",
+ "@contentDescriptionScoreOutOfPointsPossible": {
+ "description": "Formatted string for a student score out of the points possible",
+ "type": "text",
+ "placeholders_order": [
+ "score",
+ "pointsPossible"
+ ],
+ "placeholders": {
+ "score": {},
+ "pointsPossible": {}
+ }
+ },
+ "gradesSubjectMessage": "Maidir le: {studentName}, Marcanna",
+ "@gradesSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a student's grades",
+ "type": "text",
+ "placeholders_order": [
+ "studentName"
+ ],
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "syllabusSubjectMessage": "Maidir le: {studentName}, Siollabas",
+ "@syllabusSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a course syllabus",
+ "type": "text",
+ "placeholders_order": [
+ "studentName"
+ ],
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "frontPageSubjectMessage": "Maidir le: {studentName}, Leathanach Tosaigh",
+ "@frontPageSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a course front page",
+ "type": "text",
+ "placeholders_order": [
+ "studentName"
+ ],
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "assignmentSubjectMessage": "Maidir le: {studentName}, Tasc - {assignmentName}",
+ "@assignmentSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a student's assignment",
+ "type": "text",
+ "placeholders_order": [
+ "studentName",
+ "assignmentName"
+ ],
+ "placeholders": {
+ "studentName": {},
+ "assignmentName": {}
+ }
+ },
+ "eventSubjectMessage": "Maidir le: {studentName}, Imeacht - {eventTitle}",
+ "@eventSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a calendar event",
+ "type": "text",
+ "placeholders_order": [
+ "studentName",
+ "eventTitle"
+ ],
+ "placeholders": {
+ "studentName": {},
+ "eventTitle": {}
+ }
+ },
+ "There is no page information available.": "Níl aon eolas leathanaigh ar fáil.",
+ "@There is no page information available.": {
+ "description": "Description for when no page information is available",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Assignment Details": "Sonraí an Taisc",
+ "@Assignment Details": {
+ "description": "Title for the page that shows details for an assignment",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "assignmentTotalPoints": "{points} pte",
+ "@assignmentTotalPoints": {
+ "description": "Label used for the total points the assignment is worth",
+ "type": "text",
+ "placeholders_order": [
+ "points"
+ ],
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "assignmentTotalPointsAccessible": "{points} pointe",
+ "@assignmentTotalPointsAccessible": {
+ "description": "Screen reader label used for the total points the assignment is worth",
+ "type": "text",
+ "placeholders_order": [
+ "points"
+ ],
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "Due": "Dlite",
+ "@Due": {
+ "description": "Label for an assignment due date",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Grade": "Marc",
+ "@Grade": {
+ "description": "Label for the section that displays an assignment's grade",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Locked": "Faoi ghlas",
+ "@Locked": {
+ "description": "Label for when an assignment is locked",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "assignmentLockedModule": "Tá an tasc seo glasáilte ag an modúl \"{moduleName}\".",
+ "@assignmentLockedModule": {
+ "description": "The locked description when an assignment is locked by a module",
+ "type": "text",
+ "placeholders_order": [
+ "moduleName"
+ ],
+ "placeholders": {
+ "moduleName": {}
+ }
+ },
+ "Remind Me": "Cuir i gcuimhne dom",
+ "@Remind Me": {
+ "description": "Label for the row to set reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Set a date and time to be notified of this specific assignment.": "Socraigh dáta agus am le bheith curtha ar an eolas faoin tasc ar leith seo.",
+ "@Set a date and time to be notified of this specific assignment.": {
+ "description": "Description for row to set reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You will be notified about this assignment on…": "Cuirfear ar an eolas thú faoin tasc seo ar…",
+ "@You will be notified about this assignment on…": {
+ "description": "Description for when a reminder is set",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Instructions": "Treoracha",
+ "@Instructions": {
+ "description": "Label for the description of the assignment when it has quiz instructions",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Send a message about this assignment": "Seol teachtaireacht faoin tasc seo",
+ "@Send a message about this assignment": {
+ "description": "Accessibility hint for the assignment messaage floating action button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "This app is not authorized for use.": "Níl an aip seo údaraithe le húsáid.",
+ "@This app is not authorized for use.": {
+ "description": "The error shown when the app being used is not verified by Canvas",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The server you entered is not authorized for this app.": "Níl an freastalaí a d'iontráil tú údaraithe don aip seo.",
+ "@The server you entered is not authorized for this app.": {
+ "description": "The error shown when the desired login domain is not verified by Canvas",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The user agent for this app is not authorized.": "Níl an gníomhaire úsáideora don aip seo údaraithe.",
+ "@The user agent for this app is not authorized.": {
+ "description": "The error shown when the user agent during verification is not verified by Canvas",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We were unable to verify the server for use with this app.": "Níorbh fhéidir linn an freastalaí a fhíorú lena úsáid leis an aip seo.",
+ "@We were unable to verify the server for use with this app.": {
+ "description": "The generic error shown when we are unable to verify with Canvas",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Reminders": "Meabhrúcháin",
+ "@Reminders": {
+ "description": "Name of the system notification channel for assignment and event reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Notifications for reminders about assignments and calendar events": "Fógraí le haghaidh meabhrúcháin faoi thascanna agus imeachtaí féilire",
+ "@Notifications for reminders about assignments and calendar events": {
+ "description": "Description of the system notification channel for assignment and event reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Reminders have changed!": "Tá meabhrúcháin tar éis athrú!",
+ "@Reminders have changed!": {
+ "description": "Title of the dialog shown when the user needs to update their reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "In order to provide you with a better experience, we have updated how reminders work. You can add new reminders by viewing an assignment or calendar event and tapping the switch under the \"Remind Me\" section. \n\nBe aware that any reminders created with older versions of this app will not be compatible with the new changes and you will need to create them again.": "Chun eispéireas níos fearr a sholáthar duit, tá nuashonrú déanta againn ar an gcaoi a n-oibríonn meabhrúcháin. Is féidir leat meabhrúcháin nua a chur leis trí thasc nó imeacht féilire a fheiceáil agus an lasc a thapáil faoin rannán \"Cuir i gcuimhne dom\".\n\nTabhair faoi deara nach mbeidh aon mheabhrúcháin a chruthaítear le leaganacha níos sine den aip seo comhoiriúnach leis na hathruithe nua agus beidh ort iad a chruthú arís.",
+ "@In order to provide you with a better experience, we have updated how reminders work. You can add new reminders by viewing an assignment or calendar event and tapping the switch under the \"Remind Me\" section.\n\nBe aware that any reminders created with older versions of this app will not be compatible with the new changes and you will need to create them again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Not a parent?": "Ní tuismitheoir tú?",
+ "@Not a parent?": {
+ "description": "Title for the screen that shows when the user is not observing any students",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We couldn't find any students associated with this account": "Níorbh fhéidir linn aon mhac léinn a bhaineann leis an gcuntas seo a aimsiú",
+ "@We couldn't find any students associated with this account": {
+ "description": "Subtitle for the screen that shows when the user is not observing any students",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Are you a student or teacher?": "An mac léinn nó múinteoir tú?",
+ "@Are you a student or teacher?": {
+ "description": "Label for button that will show users the option to view other Canvas apps in the Play Store",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "One of our other apps might be a better fit. Tap one to visit the Play Store.": "D'fhéadfadh go mbeadh ceann dár n-aipeanna eile níos oiriúnaí. Tapáil ceann amháin chun cuairt a thabhairt ar an Play Store.",
+ "@One of our other apps might be a better fit. Tap one to visit the Play Store.": {
+ "description": "Description of options to view other Canvas apps in the Play Store",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Return to Login": "Fill ar Logáil Isteach",
+ "@Return to Login": {
+ "description": "Label for the button that returns the user to the login screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "STUDENT": "MAC LÉINN",
+ "@STUDENT": {
+ "description": "The \"student\" portion of the \"Canvas Student\" app name, in all caps. \"Canvas\" is excluded in this context as it will be displayed to the user as a wordmark image",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "TEACHER": "MÚINTEOIR",
+ "@TEACHER": {
+ "description": "The \"teacher\" portion of the \"Canvas Teacher\" app name, in all caps. \"Canvas\" is excluded in this context as it will be displayed to the user as a wordmark image",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Canvas Student": "Mac Léinn Canvas",
+ "@Canvas Student": {
+ "description": "The name of the Canvas Student app. Only \"Student\" should be translated as \"Canvas\" is a brand name in this context and should not be translated.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Canvas Teacher": "Múinteoir Canvas",
+ "@Canvas Teacher": {
+ "description": "The name of the Canvas Teacher app. Only \"Teacher\" should be translated as \"Canvas\" is a brand name in this context and should not be translated.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Alerts": "Níl aon Fholáirimh",
+ "@No Alerts": {
+ "description": "The title for the empty message to show to users when there are no alerts for the student.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There’s nothing to be notified of yet.": "Níl aon rud le cur in iúl go fóill.",
+ "@There’s nothing to be notified of yet.": {
+ "description": "The empty message to show to users when there are no alerts for the student.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "dismissAlertLabel": "Diúltaigh {alertTitle}",
+ "@dismissAlertLabel": {
+ "description": "Accessibility label to dismiss an alert",
+ "type": "text",
+ "placeholders_order": [
+ "alertTitle"
+ ],
+ "placeholders": {
+ "alertTitle": {}
+ }
+ },
+ "Course Announcement": "Fógra an Chúrsa",
+ "@Course Announcement": {
+ "description": "Title for alerts when there is a course announcement",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Institution Announcement": "Fógra Institiúide",
+ "@Institution Announcement": {
+ "description": "Title for alerts when there is an institution announcement",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "assignmentGradeAboveThreshold": "Marc an Taisc Thar {threshold}",
+ "@assignmentGradeAboveThreshold": {
+ "description": "Title for alerts when an assignment grade is above the threshold value",
+ "type": "text",
+ "placeholders_order": [
+ "threshold"
+ ],
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "assignmentGradeBelowThreshold": "Marc an Taisc Faoi {threshold}",
+ "@assignmentGradeBelowThreshold": {
+ "description": "Title for alerts when an assignment grade is below the threshold value",
+ "type": "text",
+ "placeholders_order": [
+ "threshold"
+ ],
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "courseGradeAboveThreshold": "Marc an Chúrsa Thar {threshold}",
+ "@courseGradeAboveThreshold": {
+ "description": "Title for alerts when a course grade is above the threshold value",
+ "type": "text",
+ "placeholders_order": [
+ "threshold"
+ ],
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "courseGradeBelowThreshold": "Marc an Chúrsa Faoi {threshold}",
+ "@courseGradeBelowThreshold": {
+ "description": "Title for alerts when a course grade is below the threshold value",
+ "type": "text",
+ "placeholders_order": [
+ "threshold"
+ ],
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "Settings": "Socruithe",
+ "@Settings": {
+ "description": "Title for the settings screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Theme": "Téama",
+ "@Theme": {
+ "description": "Label for the light/dark theme section in the settings page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Dark Mode": "Mód Dorcha",
+ "@Dark Mode": {
+ "description": "Label for the button that enables dark mode",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Light Mode": "Mód Geall",
+ "@Light Mode": {
+ "description": "Label for the button that enables light mode",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "High Contrast Mode": "Mód Codarsnachta Ard",
+ "@High Contrast Mode": {
+ "description": "Label for the switch that toggles high contrast mode",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Use Dark Theme in Web Content": "Úsáid Téama Dorcha in Ábhar Gréasáin",
+ "@Use Dark Theme in Web Content": {
+ "description": "Label for the switch that toggles dark mode for webviews",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Appearance": "Dealramh",
+ "@Appearance": {
+ "description": "Label for the appearance section in the settings page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Successfully submitted!": "Curtha isteach go rathúil!",
+ "@Successfully submitted!": {
+ "description": "Title displayed in the grade cell for an assignment that has been submitted",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "submissionStatusSuccessSubtitle": "Cuireadh an tasc seo isteach ar {date} ag {time} agus tá sé ag fanacht le bheith marcáilte",
+ "@submissionStatusSuccessSubtitle": {
+ "description": "Subtitle displayed in the grade cell for an assignment that has been submitted and is awaiting a grade",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "time"
+ ],
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "outOfPoints": "{howMany,plural, =1{As pointe 1}other{ as {points} pointe}}",
+ "@outOfPoints": {
+ "description": "Description for an assignment grade that has points without a current scoroe",
+ "type": "text",
+ "placeholders_order": [
+ "points",
+ "howMany"
+ ],
+ "placeholders": {
+ "points": {},
+ "howMany": {}
+ }
+ },
+ "Excused": "Leithscéal faighte",
+ "@Excused": {
+ "description": "Grading status for an assignment marked as excused",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Complete": "Críochnaithe",
+ "@Complete": {
+ "description": "Grading status for an assignment marked as complete",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Incomplete": "Neamhchríochnaithe",
+ "@Incomplete": {
+ "description": "Grading status for an assignment marked as incomplete",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "minus": "lúide",
+ "@minus": {
+ "description": "Screen reader-friendly replacement for the \"-\" character in letter grades like \"A-\"",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "latePenalty": "Pionós déanach (-{pointsLost})",
+ "@latePenalty": {
+ "description": "Text displayed when a late penalty has been applied to the assignment",
+ "type": "text",
+ "placeholders_order": [
+ "pointsLost"
+ ],
+ "placeholders": {
+ "pointsLost": {}
+ }
+ },
+ "finalGrade": "Marc Deiridh: {grade}",
+ "@finalGrade": {
+ "description": "Text that displays the final grade of an assignment",
+ "type": "text",
+ "placeholders_order": [
+ "grade"
+ ],
+ "placeholders": {
+ "grade": {}
+ }
+ },
+ "Alert Settings": "Socruithe Foláirimh",
+ "@Alert Settings": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Alert me when…": "Tabhair foláireamh dom nuair…",
+ "@Alert me when…": {
+ "description": "Header for the screen where the observer chooses the thresholds that will determine when they receive alerts (e.g. when an assignment is graded below 70%)",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Course grade below": "Marc an chúrsa thar",
+ "@Course grade below": {
+ "description": "Label describing the threshold for when the course grade is below a certain percentage",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Course grade above": "Marc an chúrsa faoi",
+ "@Course grade above": {
+ "description": "Label describing the threshold for when the course grade is above a certain percentage",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Assignment missing": "Tasc in easnamh",
+ "@Assignment missing": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Assignment grade below": "Marc an taisc faoi",
+ "@Assignment grade below": {
+ "description": "Label describing the threshold for when an assignment is graded below a certain percentage",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Assignment grade above": "Marc an taisc thar",
+ "@Assignment grade above": {
+ "description": "Label describing the threshold for when an assignment is graded above a certain percentage",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Course Announcements": "Fógraí an Chúrsa",
+ "@Course Announcements": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Institution Announcements": "Fógraí na hInstitiúide",
+ "@Institution Announcements": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Never": "Riamh",
+ "@Never": {
+ "description": "Indication that tells the user they will not receive alert notifications of a specific kind",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Grade percentage": "Céatadán an mairc",
+ "@Grade percentage": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your student's alerts.": "Tharla earráid agus foláirimh do mhic léinn á lódáil.",
+ "@There was an error loading your student's alerts.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Must be below 100": "Ní mór dó a bheith faoi 100",
+ "@Must be below 100": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "mustBeBelowN": "Ní mór dó a bheith faoi {percentage}",
+ "@mustBeBelowN": {
+ "description": "Validation error to the user that they must choose a percentage below 'n'",
+ "type": "text",
+ "placeholders_order": [
+ "percentage"
+ ],
+ "placeholders": {
+ "percentage": {
+ "example": 5
+ }
+ }
+ },
+ "mustBeAboveN": "Ní mór dó a bheith thar {percentage}",
+ "@mustBeAboveN": {
+ "description": "Validation error to the user that they must choose a percentage above 'n'",
+ "type": "text",
+ "placeholders_order": [
+ "percentage"
+ ],
+ "placeholders": {
+ "percentage": {
+ "example": 5
+ }
+ }
+ },
+ "Select Student Color": "Roghnaigh Dath an Mhic Léinn",
+ "@Select Student Color": {
+ "description": "Title for screen that allows users to assign a color to a specific student",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Electric, blue": "Leictreach, gorm",
+ "@Electric, blue": {
+ "description": "Name of the Electric (blue) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Plum, Purple": "Plumchorcra, Corcra",
+ "@Plum, Purple": {
+ "description": "Name of the Plum (purple) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Barney, Fuschia": "Barney, Fuschia",
+ "@Barney, Fuschia": {
+ "description": "Name of the Barney (fuschia) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Raspberry, Red": "Sú craobh, Dearg",
+ "@Raspberry, Red": {
+ "description": "Name of the Raspberry (red) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Fire, Orange": "Dóiteáin, Oráiste",
+ "@Fire, Orange": {
+ "description": "Name of the Fire (orange) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Shamrock, Green": "Seamróg, Glas",
+ "@Shamrock, Green": {
+ "description": "Name of the Shamrock (green) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "An error occurred while saving your selection. Please try again.": "Tharla earráid agus do rogha á shábháil. Bain triail as arís, le do thoil.",
+ "@An error occurred while saving your selection. Please try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "changeStudentColorLabel": "Athraigh dath le haghaidh {studentName}",
+ "@changeStudentColorLabel": {
+ "description": "Accessibility label for the button that lets users change the color associated with a specific student",
+ "type": "text",
+ "placeholders_order": [
+ "studentName"
+ ],
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "Teacher": "Múinteoir",
+ "@Teacher": {
+ "description": "Label for the Teacher enrollment type",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Student": "Mac léinn",
+ "@Student": {
+ "description": "Label for the Student enrollment type",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "TA": "CT",
+ "@TA": {
+ "description": "Label for the Teaching Assistant enrollment type (also known as Teacher Aid or Education Assistant), reduced to a short acronym/initialism if appropriate.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Observer": "Breathnóir",
+ "@Observer": {
+ "description": "Label for the Observer enrollment type",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Use Camera": "Úsáid Ceamara",
+ "@Use Camera": {
+ "description": "Label for the action item that lets the user capture a photo using the device camera",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Upload File": "Uaslódáil Comhad",
+ "@Upload File": {
+ "description": "Label for the action item that lets the user upload a file from their device",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Choose from Gallery": "Roghnaigh ón nGailearaí",
+ "@Choose from Gallery": {
+ "description": "Label for the action item that lets the user select a photo from their device gallery",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Preparing…": "Ag ullmhú…",
+ "@Preparing…": {
+ "description": "Message shown while a file is being prepared to attach to a message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Add student with…": "Cuir mac léinn le…",
+ "@Add student with…": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Add Student": "Cuir Mac Léinn leis",
+ "@Add Student": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You are not observing any students.": "Níl tú ag breathnú ar mhac léinn ar bith.",
+ "@You are not observing any students.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your students.": "Tharla earráid agus do mhic léinn á lódáil.",
+ "@There was an error loading your students.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Pairing Code": "Cód Péireála",
+ "@Pairing Code": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Students can obtain a pairing code through the Canvas website": "Is féidir le mic léinn cód péireála a fháil trí shuíomh Gréasáin Canvas",
+ "@Students can obtain a pairing code through the Canvas website": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Enter the student pairing code provided to you. If the pairing code doesn't work, it may have expired": "Cuir isteach an cód péireála mac léinn a thugtar duit. Mura n-oibríonn an cód péireála, seans go mbeidh sé imithe in éag",
+ "@Enter the student pairing code provided to you. If the pairing code doesn't work, it may have expired": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Your code is incorrect or expired.": "Tá do chód mícheart nó imithe in éag.",
+ "@Your code is incorrect or expired.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Something went wrong trying to create your account, please reach out to your school for assistance.": "Tharla earráid agus do chuntas a chruthú, déan teagmháil le do scoil le haghaidh cabhrach.",
+ "@Something went wrong trying to create your account, please reach out to your school for assistance.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "QR Code": "Cód MF",
+ "@QR Code": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Students can create a QR code using the Canvas Student app on their mobile device": "Is féidir le mic léinn cód MF a chruthú ag baint úsáide as an aip Canvas Mac Léinn ar a ngléas soghluaiste",
+ "@Students can create a QR code using the Canvas Student app on their mobile device": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Add new student": "Cuir mac léinn nua leis",
+ "@Add new student": {
+ "description": "Semantics label for the FAB on the Manage Students Screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Select": "Roghnaigh",
+ "@Select": {
+ "description": "Hint text to tell the user to choose one of two options",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I have a Canvas account": "Tá cuntas Canvas agam",
+ "@I have a Canvas account": {
+ "description": "Option to select for users that have a canvas account",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I don't have a Canvas account": "Níl cuntas Canvas agam",
+ "@I don't have a Canvas account": {
+ "description": "Option to select for users that don't have a canvas account",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Create Account": "Cruthaigh Cuntas",
+ "@Create Account": {
+ "description": "Button text for account creation confirmation",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Full Name": "Ainm iomlán",
+ "@Full Name": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email Address": "Seoladh Ríomhphoist",
+ "@Email Address": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Password": "Pasfhocal",
+ "@Password": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Full Name…": "Ainm iomlán…",
+ "@Full Name…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email…": "Ríomhphost…",
+ "@Email…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Password…": "Pasfhocal…",
+ "@Password…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Please enter full name": "Cuir isteach ainm iomlán",
+ "@Please enter full name": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Please enter an email address": "Cuir isteach seoladh ríomhphoist",
+ "@Please enter an email address": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Please enter a valid email address": "Cuir isteach seoladh ríomhphoist bailí",
+ "@Please enter a valid email address": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Password is required": "Tá pasfhocal ag teastáil",
+ "@Password is required": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Password must contain at least 8 characters": "Caithfidh 8 gcarachtar ar a laghad a bheith sa phasfhocal",
+ "@Password must contain at least 8 characters": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "qrCreateAccountTos": "Trí ‘Cruthaigh Cuntas’ a thapáil, aontaíonn tú leis an {termsOfService} agus aithníonn tú an {privacyPolicy}",
+ "@qrCreateAccountTos": {
+ "description": "The text show on the account creation screen",
+ "type": "text",
+ "placeholders_order": [
+ "termsOfService",
+ "privacyPolicy"
+ ],
+ "placeholders": {
+ "termsOfService": {},
+ "privacyPolicy": {}
+ }
+ },
+ "Terms of Service": "Tearmaí Seirbhíse",
+ "@Terms of Service": {
+ "description": "Label for the Canvas Terms of Service agreement. This will be used in the qrCreateAccountTos text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Privacy Policy": "Polasaí Príobháideachais",
+ "@Privacy Policy": {
+ "description": "Label for the Canvas Privacy Policy agreement. This will be used in the qrCreateAccountTos text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "View the Privacy Policy": "Féach ar an bPolasaí Príobháideachais",
+ "@View the Privacy Policy": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Already have an account? ": "An bhfuil cuntas agat cheana féin? ",
+ "@Already have an account? ": {
+ "description": "Part of multiline text span, includes AccountSignIn1-2, in that order",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Sign In": "Sínigh isteach",
+ "@Sign In": {
+ "description": "Part of multiline text span, includes AccountSignIn1-2, in that order",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Hide Password": "Folaigh Pasfhocal",
+ "@Hide Password": {
+ "description": "content description for password hide button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Show Password": "Taispeáin Pasfhocal",
+ "@Show Password": {
+ "description": "content description for password show button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Terms of Service Link": "Nasc Téarmaí Seirbhíse",
+ "@Terms of Service Link": {
+ "description": "content description for terms of service link",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Privacy Policy Link": "Nasc Polasaí Príobháideachais",
+ "@Privacy Policy Link": {
+ "description": "content description for privacy policy link",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Event": "Imeacht",
+ "@Event": {
+ "description": "Title for the event details screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Date": "Dáta",
+ "@Date": {
+ "description": "Label for the event date",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Location": "Suíomh",
+ "@Location": {
+ "description": "Label for the location information",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Location Specified": "Níl Suíomh Sonraithe",
+ "@No Location Specified": {
+ "description": "Description for events that do not have a location",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "eventTime": "{startAt} - {endAt}",
+ "@eventTime": {
+ "description": "The time the event is happening, example: \"2:00 pm - 4:00 pm\"",
+ "type": "text",
+ "placeholders_order": [
+ "startAt",
+ "endAt"
+ ],
+ "placeholders": {
+ "startAt": {},
+ "endAt": {}
+ }
+ },
+ "Set a date and time to be notified of this event.": "Socraigh dáta agus am le cur ar an eolas faoin imeacht seo.",
+ "@Set a date and time to be notified of this event.": {
+ "description": "Description for row to set event reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You will be notified about this event on…": "Cuirfear ar an eolas thú faoin imeacht seo ar…",
+ "@You will be notified about this event on…": {
+ "description": "Description for when an event reminder is set",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Share Your Love for the App": "Comhroinn do Grá don Aip",
+ "@Share Your Love for the App": {
+ "description": "Label for option to open the app store",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tell us about your favorite parts of the app": "Inis dúinn faoi na cuideanna is fearr leat den aip",
+ "@Tell us about your favorite parts of the app": {
+ "description": "Description for option to open the app store",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Legal": "Dlíthiúil",
+ "@Legal": {
+ "description": "Label for legal information option",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Privacy policy, terms of use, open source": "Polasaí príobháideachais, téarmaí úsáide, foinse oscailte",
+ "@Privacy policy, terms of use, open source": {
+ "description": "Description for legal information option",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Idea for Canvas Parent App [Android]": "Smaoineamh don Aip Máthairchúrsa Canvas [Android]",
+ "@Idea for Canvas Parent App [Android]": {
+ "description": "The subject for the email to request a feature",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The following information will help us better understand your idea:": "Cabhróidh an t-eolas seo a leanas linn do smaoineamh a thuiscint níos fearr:",
+ "@The following information will help us better understand your idea:": {
+ "description": "The header for the users information that is attached to a feature request",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Domain:": "Fearann:",
+ "@Domain:": {
+ "description": "The label for the Canvas domain of the logged in user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "User ID:": "ID an Úsáideora:",
+ "@User ID:": {
+ "description": "The label for the Canvas user ID of the logged in user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email:": "Ríomhphost:",
+ "@Email:": {
+ "description": "The label for the eamil of the logged in user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Locale:": "Suíomh:",
+ "@Locale:": {
+ "description": "The label for the locale of the logged in user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Terms of Use": "Téarmaí Úsáide",
+ "@Terms of Use": {
+ "description": "Label for the terms of use",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Canvas on GitHub": "Canvas ar GitHub",
+ "@Canvas on GitHub": {
+ "description": "Label for the button that opens the Canvas project on GitHub's website",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was a problem loading the Terms of Use": "Bhí fadhb ann agus na Téarmaí Úsáide á lódáil",
+ "@There was a problem loading the Terms of Use": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Device": "Gléas",
+ "@Device": {
+ "description": "Label used for device manufacturer/model in the error report",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "OS Version": "Leagan OS",
+ "@OS Version": {
+ "description": "Label used for device operating system version in the error report",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Version Number": "Uimhir Leagan",
+ "@Version Number": {
+ "description": "Label used for the app version number in the error report",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Report A Problem": "Tuairiscigh Fadhb",
+ "@Report A Problem": {
+ "description": "Title used for generic dialog to report problems",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Subject": "Ábhar",
+ "@Subject": {
+ "description": "Label used for Subject text field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "A subject is required.": "Tá ábhar ag teastáil.",
+ "@A subject is required.": {
+ "description": "Error shown when the subject field is empty",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "An email address is required.": "Tá seoladh ríomhphoist ag teastáil.",
+ "@An email address is required.": {
+ "description": "Error shown when the email field is empty",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Description": "Cur síos",
+ "@Description": {
+ "description": "Label used for Description text field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "A description is required.": "Tá cur síos ag teastáil.",
+ "@A description is required.": {
+ "description": "Error shown when the description field is empty",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "How is this affecting you?": "Cén tionchar atá aige seo ort?",
+ "@How is this affecting you?": {
+ "description": "Label used for the dropdown to select how severe the issue is",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "send": "seol",
+ "@send": {
+ "description": "Label used for send button when reporting a problem",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Just a casual question, comment, idea, suggestion…": "Níl ann ach ceist ócáideach, ráiteas, smaoineamh, moladh…",
+ "@Just a casual question, comment, idea, suggestion…": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I need some help but it's not urgent.": "Tá roinnt cabhrach de dhíth orm ach níl sé práinneach.",
+ "@I need some help but it's not urgent.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Something's broken but I can work around it to get what I need done.": "Tá rud éigin briste ach is féidir liom oibriú timpeall air chun an méid a theastaíonn uaim a dhéanamh.",
+ "@Something's broken but I can work around it to get what I need done.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I can't get things done until I hear back from you.": "Ní féidir liom rudaí a dhéanamh go dtí go gcloisim ar ais uait.",
+ "@I can't get things done until I hear back from you.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "EXTREME CRITICAL EMERGENCY!!": "ÉIGEANDÁIL FÍORCHRITICIÚIL!!",
+ "@EXTREME CRITICAL EMERGENCY!!": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Not Graded": "Níl sé Marcáilte",
+ "@Not Graded": {
+ "description": "Description for an assignment has not been graded.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login flow: Normal": "Sreabhadh logáil isteach: Gnáth",
+ "@Login flow: Normal": {
+ "description": "Description for the normal login flow",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login flow: Canvas": "Sreabhadh logáil isteach: Canvas",
+ "@Login flow: Canvas": {
+ "description": "Description for the Canvas login flow",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login flow: Site Admin": "Sreabhadh logáil isteach: Riarthóir an tSuímh",
+ "@Login flow: Site Admin": {
+ "description": "Description for the Site Admin login flow",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login flow: Skip mobile verify": "Sreabhadh logáil isteach: Scipeáil deimhniú soghluaiste",
+ "@Login flow: Skip mobile verify": {
+ "description": "Description for the login flow that skips domain verification for mobile",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Act As User": "Gníomh mar Úsáideoir",
+ "@Act As User": {
+ "description": "Label for the button that allows the user to act (masquerade) as another user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Stop Acting as User": "Stop ag Gníomhú mar Úsáideoir",
+ "@Stop Acting as User": {
+ "description": "Label for the button that allows the user to stop acting (masquerading) as another user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "actingAsUser": "Tá tú ag gníomhú mar {userName}",
+ "@actingAsUser": {
+ "description": "Message shown while acting (masquerading) as another user",
+ "type": "text",
+ "placeholders_order": [
+ "userName"
+ ],
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "\"Act as\" is essentially logging in as this user without a password. You will be able to take any action as if you were this user, and from other users' points of views, it will be as if this user performed them. However, audit logs record that you were the one who performed the actions on behalf of this user.": "Go bunúsach is éard atá i gceist le \"Gníomh mar\" logáil isteach mar an úsáideoir seo gan pasfhocal. Beidh tú in ann aon ghníomg a dhéanamh amhail is gur tusa an t-úsáideoir seo, agus ó dhearcadh úsáideoirí eile, beidh sé mar a rinne an t-úsáideoir seo iad. Mar sin féin, taifeadann logaí iniúchta gurb tusa an té a rinne na gníomhartha thar ceann an úsáideora seo.",
+ "@\"Act as\" is essentially logging in as this user without a password. You will be able to take any action as if you were this user, and from other users' points of views, it will be as if this user performed them. However, audit logs record that you were the one who performed the actions on behalf of this user.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Domain": "Fearann",
+ "@Domain": {
+ "description": "Text field hint for domain url input",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You must enter a valid domain": "Ní mór duit fearann bailí a chur isteach",
+ "@You must enter a valid domain": {
+ "description": "Message displayed for domain input error",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "User ID": "ID an Úsáideora",
+ "@User ID": {
+ "description": "Text field hint for user ID input",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You must enter a user id": "Ní mór duit id an úsáideora a chur isteach",
+ "@You must enter a user id": {
+ "description": "Message displayed for user Id input error",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error trying to act as this user. Please check the Domain and User ID and try again.": "Tharla earráid agus iarracht á déanamh mar úsáideoir seo. Seiceáil an Fearainn agus ID an Úsáideora agus bain triail eile as.",
+ "@There was an error trying to act as this user. Please check the Domain and User ID and try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "endMasqueradeMessage": "Stopfaidh tú ag gníomhú mar {userName} agus filleadh ar do chuntas bunaidh.",
+ "@endMasqueradeMessage": {
+ "description": "Confirmation message displayed when the user wants to stop acting (masquerading) as another user",
+ "type": "text",
+ "placeholders_order": [
+ "userName"
+ ],
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "endMasqueradeLogoutMessage": "Stopfaidh tú ag gníomhú mar {userName} agus logálfar amach thú.",
+ "@endMasqueradeLogoutMessage": {
+ "description": "Confirmation message displayed when the user wants to stop acting (masquerading) as another user and will be logged out.",
+ "type": "text",
+ "placeholders_order": [
+ "userName"
+ ],
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "How are we doing?": "Conas atá ag éirí linn?",
+ "@How are we doing?": {
+ "description": "Title for dialog asking user to rate the app out of 5 stars.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Don't show again": "Ná taispeáin arís",
+ "@Don't show again": {
+ "description": "Button to prevent the rating dialog from showing again.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "What can we do better?": "Cad is féidir linn a dhéanamh níos fearr?",
+ "@What can we do better?": {
+ "description": "Hint text for providing a comment with the rating.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Send Feedback": "Seol Aiseolas",
+ "@Send Feedback": {
+ "description": "Button to send rating with feedback",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "ratingDialogEmailSubject": "Moltaí le haghaidh Android - Máthairchúrsa Canvas {version}",
+ "@ratingDialogEmailSubject": {
+ "description": "The subject for an email to provide feedback for CanvasParent.",
+ "type": "text",
+ "placeholders_order": [
+ "version"
+ ],
+ "placeholders": {
+ "version": {}
+ }
+ },
+ "starRating": "{position,plural, =1{{position} réalta}other{{position} réalta}}",
+ "@starRating": {
+ "description": "Accessibility label for the 1 stars to 5 stars rating",
+ "type": "text",
+ "placeholders_order": [
+ "position"
+ ],
+ "placeholders": {
+ "position": {
+ "example": 1
+ }
+ }
+ },
+ "Student Pairing": "Péireáil Mac Léinn",
+ "@Student Pairing": {
+ "description": "Title for the screen where users can pair to students using a QR code",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Open Canvas Student": "Oscail Mac Léinn Canvas",
+ "@Open Canvas Student": {
+ "description": "Title for QR pairing tutorial screen instructing users to open the Canvas Student app",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You'll need to open your student's Canvas Student app to continue. Go into Main Menu > Settings > Pair with Observer and scan the QR code you see there.": "Beidh ort aip Mac Léinn Canvas do mhic léinn a oscailt chun leanúint ar aghaidh. Téigh isteach sa Phríomh-Roghchlár > Socruithe > Péireáil le Breathnóir agus scanadh an cód MF a fheiceann tú ann.",
+ "@You'll need to open your student's Canvas Student app to continue. Go into Main Menu > Settings > Pair with Observer and scan the QR code you see there.": {
+ "description": "Message explaining how QR code pairing works",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Screenshot showing location of pairing QR code generation in the Canvas Student app": "Gabháil scáileáin a thaispeánann an áit inar péireáladh giniúint cód MF san aip Canvas Mac Léinn",
+ "@Screenshot showing location of pairing QR code generation in the Canvas Student app": {
+ "description": "Content Description for qr pairing tutorial screenshot",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Expired QR Code": "Cód MF imithe in éag",
+ "@Expired QR Code": {
+ "description": "Error title shown when the users scans a QR code that has expired",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The QR code you scanned may have expired. Refresh the code on the student's device and try again.": "Seans go bhfuil an cód MF a scanáil tú imithe in éag. Athnuaigh an cód ar ghléas an mac léinn agus bain triail eile as.",
+ "@The QR code you scanned may have expired. Refresh the code on the student's device and try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "A network error occurred when adding this student. Check your connection and try again.": "Tharla earráid ghréasáin agus an mac léinn seo á chur leis. Seiceáil do cheangal agus bain triail eile as.",
+ "@A network error occurred when adding this student. Check your connection and try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Invalid QR Code": "Cód MF neamhbhailí",
+ "@Invalid QR Code": {
+ "description": "Error title shown when the user scans an invalid QR code",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Incorrect Domain": "Fearann Mícheart",
+ "@Incorrect Domain": {
+ "description": "Error title shown when the users scane a QR code for a student that belongs to a different domain",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The student you are trying to add belongs to a different school. Log in or create an account with that school to scan this code.": "Is le scoil eile é an mac léinn seo a bhfuil tú ag iarraidh a chur leis. Logáil isteach nó cruthaigh cuntas leis an scoil sin chun an cód seo a scanadh.",
+ "@The student you are trying to add belongs to a different school. Log in or create an account with that school to scan this code.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Camera Permission": "Cead Ceamara",
+ "@Camera Permission": {
+ "description": "Error title shown when the user wans to scan a QR code but has denied the camera permission",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "This will unpair and remove all enrollments for this student from your account.": "Déanfaidh sé seo na rolluithe ar fad don mhac léinn seo a dhíphéireáil agus a bhaint de do chuntas.",
+ "@This will unpair and remove all enrollments for this student from your account.": {
+ "description": "Confirmation message shown when the user tries to delete a student from their account",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was a problem removing this student from your account. Please check your connection and try again.": "Bhí fadhb ann an mac léinn seo a bhaint de do chuntas. Seiceáil do cheangal agus bain triail eile as.",
+ "@There was a problem removing this student from your account. Please check your connection and try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Cancel": "Cealaigh",
+ "@Cancel": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "next": "Ar aghaidh",
+ "@next": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "ok": "CGL",
+ "@ok": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Yes": "Tá",
+ "@Yes": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No": "Níl",
+ "@No": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Retry": "Bain triail eile as",
+ "@Retry": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Delete": "Scrios",
+ "@Delete": {
+ "description": "Label used for general delete/remove actions",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Done": "Déanta",
+ "@Done": {
+ "description": "Label for general done/finished actions",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Refresh": "Athnuaigh",
+ "@Refresh": {
+ "description": "Label for button to refresh data from the web",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "View Description": "Féach ar Chur síos",
+ "@View Description": {
+ "description": "Button to view the description for an event or assignment",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "expanded": "leathnaithe",
+ "@expanded": {
+ "description": "Description for the accessibility reader for list groups that are expanded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "collapsed": "leacaithe",
+ "@collapsed": {
+ "description": "Description for the accessibility reader for list groups that are expanded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "An unexpected error occurred": "Tharla earráid gan choinne",
+ "@An unexpected error occurred": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No description": "Gan cuir síos",
+ "@No description": {
+ "description": "Message used when the assignment has no description",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Launch External Tool": "Seol Uirlis Sheachtrach",
+ "@Launch External Tool": {
+ "description": "Button text added to webviews to let users open external tools in their browser",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Interactions on this page are limited by your institution.": "Tá idirghníomhaíochtaí ar an leathanach seo teoranta ag d'institiúid.",
+ "@Interactions on this page are limited by your institution.": {
+ "description": "Message describing how the webview has limited access due to an instution setting",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "dateAtTime": "{date} ag {time}",
+ "@dateAtTime": {
+ "description": "The string to format dates",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "time"
+ ],
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "dueDateAtTime": "Dlite {date} ag {time}",
+ "@dueDateAtTime": {
+ "description": "The string to format due dates",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "time"
+ ],
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "No Due Date": "Níl Dáta Dlite",
+ "@No Due Date": {
+ "description": "Label for assignments that do not have a due date",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Filter": "Scag",
+ "@Filter": {
+ "description": "Label for buttons to filter what items are visible",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "unread": "neamhléite",
+ "@unread": {
+ "description": "Label for things that are marked as unread",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "unreadCount": "{count} neamhléite",
+ "@unreadCount": {
+ "description": "Formatted string for when there are a number of unread items",
+ "type": "text",
+ "placeholders_order": [
+ "count"
+ ],
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "badgeNumberPlus": "{count}+",
+ "@badgeNumberPlus": {
+ "description": "Formatted string for when too many items are being notified in a badge, generally something like: 99+",
+ "type": "text",
+ "placeholders_order": [
+ "count"
+ ],
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "There was an error loading this announcement": "Tharla earráid agus an fógra seo á lódáil",
+ "@There was an error loading this announcement": {
+ "description": "Message shown when an announcement detail screen fails to load",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Network error": "Earráid ghréasáin",
+ "@Network error": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Under Construction": "Á dTógáil",
+ "@Under Construction": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We are currently building this feature for your viewing pleasure.": "Táimid ag tógáil an ghné seo faoi láthair chun do phléisiúr féachana.",
+ "@We are currently building this feature for your viewing pleasure.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Request Login Help Button": "Cnaipe Iarr Cabhair ag Logáil Isteach",
+ "@Request Login Help Button": {
+ "description": "Accessibility hint for button that opens help dialog for a login help request",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Request Login Help": "Iarr Cabhair Logála Isteach",
+ "@Request Login Help": {
+ "description": "Title of help dialog for a login help request",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I'm having trouble logging in": "Tá deacracht agam logáil isteach",
+ "@I'm having trouble logging in": {
+ "description": "Subject of help dialog for a login help request",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "An error occurred when trying to display this link": "Tharla earráid agus an nasc seo á thaispeáint",
+ "@An error occurred when trying to display this link": {
+ "description": "Error message shown when a link can't be opened",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We are unable to display this link, it may belong to an institution you currently aren't logged in to.": "Ní féidir linn an nasc seo a thaispeáint, seans gur le hinstitiúid nach bhfuil tú logáilte isteach inti faoi láthair.",
+ "@We are unable to display this link, it may belong to an institution you currently aren't logged in to.": {
+ "description": "Description for error page shown when clicking a link",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Link Error": "Earráid Naisc",
+ "@Link Error": {
+ "description": "Title for error page shown when clicking a link",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Open In Browser": "Oscail i mBrabhsálaí",
+ "@Open In Browser": {
+ "description": "Text for button to open a link in the browswer",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You'll find the QR code on the web in your account profile. Click 'QR for Mobile Login' in the list.": "Gheobhaidh tú an cód MF ar an ngréasán i do phróifíl chuntais. Cliceáil 'MF le haghaidh Logáil Isteach Móibíleach' sa liosta.",
+ "@You'll find the QR code on the web in your account profile. Click 'QR for Mobile Login' in the list.": {
+ "description": "Text for qr login tutorial screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Locate QR Code": "Aimsigh Cód MF",
+ "@Locate QR Code": {
+ "description": "Text for qr login button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Please scan a QR code generated by Canvas": "Scan cód MF ginte ag Canvas",
+ "@Please scan a QR code generated by Canvas": {
+ "description": "Text for qr login error with incorrect qr code",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error logging in. Please generate another QR Code and try again.": "Tharla earráid agus logáil isteach. Gin Cód MF eile agus bain triail eile as.",
+ "@There was an error logging in. Please generate another QR Code and try again.": {
+ "description": "Text for qr login error",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Screenshot showing location of QR code generation in browser": "Seit scáileáin a thaispeánann suíomh giniúna an chóid MF sa bhrabhsálaí",
+ "@Screenshot showing location of QR code generation in browser": {
+ "description": "Content Description for qr login tutorial screenshot",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "QR scanning requires camera access": "Teastaíonn rochtain ceamara le haghaidh scanadh MF",
+ "@QR scanning requires camera access": {
+ "description": "placeholder for camera error for QR code scan",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The linked item is no longer available": "Níl an mhír nasctha ar fáil a thuilleadh",
+ "@The linked item is no longer available": {
+ "description": "error message when the alert could no be opened",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Message sent": "Teachtaireacht seolta",
+ "@Message sent": {
+ "description": "confirmation message on the screen when the user succesfully sends a message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Acceptable Use Policy": "Beartas Úsáide Sothuigthe",
+ "@Acceptable Use Policy": {
+ "description": "title for the acceptable use policy screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Submit": "Cuir isteach",
+ "@Submit": {
+ "description": "submit button title for acceptable use policy screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": "Is úsáideoir nua thú nó tá an Beartas um Úsáid Inghlactha athraithe ó d’aontaigh tú leis an uair dheireanach. Aontaigh leis an mBeartas um Úsáid Inghlactha sula leanann tú ar aghaidh.",
+ "@Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": {
+ "description": "acceptable use policy screen description",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I agree to the Acceptable Use Policy.": "Aontaím leis an mBeartas um Úsáid Inghlactha.",
+ "@I agree to the Acceptable Use Policy.": {
+ "description": "acceptable use policy switch title",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "About": "Faoi",
+ "@About": {
+ "description": "Title for about menu item in settings",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "App": "Aip",
+ "@App": {
+ "description": "Title for App field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login ID": "ID Logáil Isteach",
+ "@Login ID": {
+ "description": "Title for Login ID field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email": "Ríomhphost",
+ "@Email": {
+ "description": "Title for Email field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Version": "Leagan",
+ "@Version": {
+ "description": "Title for Version field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Instructure logo": "Lógó Instructure",
+ "@Instructure logo": {
+ "description": "Semantics label for the Instructure logo on the about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/flutter_parent/lib/l10n/res/intl_hi.arb b/apps/flutter_parent/lib/l10n/res/intl_hi.arb
new file mode 100644
index 0000000000..bc0b681b83
--- /dev/null
+++ b/apps/flutter_parent/lib/l10n/res/intl_hi.arb
@@ -0,0 +1,2753 @@
+{
+ "@@last_modified": "2023-08-25T11:04:20.901151",
+ "alertsLabel": "चेतावनियां",
+ "@alertsLabel": {
+ "description": "The label for the Alerts tab",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "calendarLabel": "कैलेंडर",
+ "@calendarLabel": {
+ "description": "The label for the Calendar tab",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "coursesLabel": "पाठ्यक्रम",
+ "@coursesLabel": {
+ "description": "The label for the Courses tab",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Students": "कोई छात्र नहीं",
+ "@No Students": {
+ "description": "Text for when an observer has no students they are observing",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tap to show student selector": "छात्र चयनकर्ता दिखाने के लिए टैप करें",
+ "@Tap to show student selector": {
+ "description": "Semantics label for the area that will show the student selector when tapped",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tap to pair with a new student": "किसी नए छात्र के साथ जुड़ने के लिए टैप करें",
+ "@Tap to pair with a new student": {
+ "description": "Semantics label for the add student button in the student selector",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tap to select this student": "इस छात्र को चुनने केलिए टैप करें",
+ "@Tap to select this student": {
+ "description": "Semantics label on individual students in the student switcher",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Manage Students": "छात्र प्रबंधित करें",
+ "@Manage Students": {
+ "description": "Label text for the Manage Students nav drawer button as well as the title for the Manage Students screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Help": "मदद",
+ "@Help": {
+ "description": "Label text for the help nav drawer button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Log Out": "लॉग आउट करें",
+ "@Log Out": {
+ "description": "Label text for the Log Out nav drawer button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Switch Users": "उपयोगकर्ता स्विच करें",
+ "@Switch Users": {
+ "description": "Label text for the Switch Users nav drawer button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "appVersion": "v. {version}",
+ "@appVersion": {
+ "description": "App version shown in the navigation drawer",
+ "type": "text",
+ "placeholders_order": [
+ "version"
+ ],
+ "placeholders": {
+ "version": {}
+ }
+ },
+ "Are you sure you want to log out?": "क्या आप सच में लॉग आउट होना चाहते हैं?",
+ "@Are you sure you want to log out?": {
+ "description": "Confirmation message displayed when the user tries to log out",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Calendars": "कैलेंडर",
+ "@Calendars": {
+ "description": "Label for button that lets users select which calendars to display",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "nextMonth": "अगला महीना: {month}",
+ "@nextMonth": {
+ "description": "Label for the button that switches the calendar to the next month",
+ "type": "text",
+ "placeholders_order": [
+ "month"
+ ],
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "previousMonth": "पिछला महीना: {month}",
+ "@previousMonth": {
+ "description": "Label for the button that switches the calendar to the previous month",
+ "type": "text",
+ "placeholders_order": [
+ "month"
+ ],
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "nextWeek": "अगला सप्ताह {date} से शुरू",
+ "@nextWeek": {
+ "description": "Label for the button that switches the calendar to the next week",
+ "type": "text",
+ "placeholders_order": [
+ "date"
+ ],
+ "placeholders": {
+ "date": {}
+ }
+ },
+ "previousWeek": "पिछला सप्ताह {date} से शुरू",
+ "@previousWeek": {
+ "description": "Label for the button that switches the calendar to the previous week",
+ "type": "text",
+ "placeholders_order": [
+ "date"
+ ],
+ "placeholders": {
+ "date": {}
+ }
+ },
+ "selectedMonthLabel": "{month} का महीना",
+ "@selectedMonthLabel": {
+ "description": "Accessibility label for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders_order": [
+ "month"
+ ],
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "expand": "बढ़ाएं",
+ "@expand": {
+ "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "collapse": "संक्षिप्त करें",
+ "@collapse": {
+ "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "pointsPossible": "{points} पॉइंट्स संभव हैं",
+ "@pointsPossible": {
+ "description": "Screen reader label used for the points possible for an assignment, quiz, etc.",
+ "type": "text",
+ "placeholders_order": [
+ "points"
+ ],
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} घटना}other{{date}, {eventCount} घटनाएं}}",
+ "@calendarDaySemanticsLabel": {
+ "description": "Screen reader label used for calendar day, reads the date and count of events",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "eventCount"
+ ],
+ "placeholders": {
+ "date": {},
+ "eventCount": {}
+ }
+ },
+ "No Events Today!": "आज कोई घटना नहीं है!",
+ "@No Events Today!": {
+ "description": "Title displayed when there are no calendar events for the current day",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "It looks like a great day to rest, relax, and recharge.": "आज का दिन आराम, विश्राम करने और नई ऊर्जा पाने के लिए अच्छा लग रहा है।",
+ "@It looks like a great day to rest, relax, and recharge.": {
+ "description": "Message displayed when there are no calendar events for the current day",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your student's calendar": "आपके छात्र के कैलेंडर को लोड करने में त्रुटि हुई",
+ "@There was an error loading your student's calendar": {
+ "description": "Message displayed when calendar events could not be loaded for the current student",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tap to favorite the courses you want to see on the Calendar. Select up to 10.": "उन पाठ्यक्रमों को पसंदीदा बनाने के लिए टैप करें जिन्हें आप कैलेंडर पर देखना चाहते हैं। अधिकतम 10 चुनें।",
+ "@Tap to favorite the courses you want to see on the Calendar. Select up to 10.": {
+ "description": "Description text on calendar filter screen.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You may only choose 10 calendars to display": "आप प्रदर्शित करने के लिए केवल 10 कैलेंडर चुन सकते हैं",
+ "@You may only choose 10 calendars to display": {
+ "description": "Error text when trying to select more than 10 calendars",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You must select at least one calendar to display": "आपको प्रदर्शित करने के लिए कम से कम एक कैलेंडर का चयन करना होगा",
+ "@You must select at least one calendar to display": {
+ "description": "Error text when trying to de-select all calendars",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Planner Note": "योजनाकर्ता नोट",
+ "@Planner Note": {
+ "description": "Label used for notes in the planner",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Go to today": "आज पर जाएं",
+ "@Go to today": {
+ "description": "Accessibility label used for the today button in the planner",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Previous Logins": "पिछले लॉगिन",
+ "@Previous Logins": {
+ "description": "Label for the list of previous user logins",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "canvasLogoLabel": "Canvas लोगो",
+ "@canvasLogoLabel": {
+ "description": "The semantics label for the Canvas logo",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "findSchool": "स्कूल ढूंढें",
+ "@findSchool": {
+ "description": "Text for the find-my-school button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "findAnotherSchool": "कोई अन्य स्कूल ढूंढें",
+ "@findAnotherSchool": {
+ "description": "Text for the find-another-school button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "domainSearchInputHint": "स्कूल का नाम या डिस्ट्रिक्ट दर्ज करें…",
+ "@domainSearchInputHint": {
+ "description": "Input hint for the text box on the domain search screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "noDomainResults": "\"{query}\" से मेल खाते स्कूल ढूंढने में असमर्थ ",
+ "@noDomainResults": {
+ "description": "Message shown to users when the domain search query did not return any results",
+ "type": "text",
+ "placeholders_order": [
+ "query"
+ ],
+ "placeholders": {
+ "query": {}
+ }
+ },
+ "domainSearchHelpLabel": "मैं अपना स्कूल या डिस्ट्रिक्ट कैसे ढूंढूं?",
+ "@domainSearchHelpLabel": {
+ "description": "Label for the help button on the domain search screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "canvasGuides": "Canvas गाइड",
+ "@canvasGuides": {
+ "description": "Proper name for the Canvas Guides. This will be used in the domainSearchHelpBody text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "canvasSupport": "Canvas सपोर्ट",
+ "@canvasSupport": {
+ "description": "Proper name for Canvas Support. This will be used in the domainSearchHelpBody text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "domainSearchHelpBody": "आप जिस स्कूल या डिस्ट्रिक्ट को एक्सेस करने का प्रयास कर रहे हैं उसका नाम खोजने का प्रयास करें, जैसे \"Smith प्राइवेट स्कूल\" या \"Smith काउंटी स्कूल\"। आप सीधे Canvas डोमेन भी दर्ज कर सकते हैं, जैसे \"smith.instructure.com\" \n\nअपने संस्थान के Canvas खाते को खोजने के बारे में अधिक जानकारी के लिए, आप {canvasGuides} पर जा सकते हैं, {canvasSupport} से संपर्क कर सकते हैं, या सहायता के लिए अपने स्कूल से संपर्क कर सकते हैं।",
+ "@domainSearchHelpBody": {
+ "description": "The body text shown in the help dialog on the domain search screen",
+ "type": "text",
+ "placeholders_order": [
+ "canvasGuides",
+ "canvasSupport"
+ ],
+ "placeholders": {
+ "canvasGuides": {},
+ "canvasSupport": {}
+ }
+ },
+ "Uh oh!": "उह ओह!",
+ "@Uh oh!": {
+ "description": "Title of the screen that shows when a crash has occurred",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "हमें नहीं पता कि क्या हुआ, पर जो हुआ अच्छा नहीं था। यदि ऐसा बार-बार हो, तो हमसे संपर्क करें।",
+ "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": {
+ "description": "Message shown when a crash has occurred",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Contact Support": "सपोर्ट से संपर्क करें",
+ "@Contact Support": {
+ "description": "Label for the button that allows users to contact support after a crash has occurred",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "View error details": "त्रुटि विवरण देखें",
+ "@View error details": {
+ "description": "Label for the button that allowed users to view crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Restart app": "ऐप फिर से शुरू करें",
+ "@Restart app": {
+ "description": "Label for the button that will restart the entire application",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Application version": "ऐप्लिकेशन संस्करण",
+ "@Application version": {
+ "description": "Label for the application version displayed in the crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Device model": "डिवाइस मॉडल",
+ "@Device model": {
+ "description": "Label for the device model displayed in the crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Android OS version": "Android OS संस्करण",
+ "@Android OS version": {
+ "description": "Label for the Android operating system version displayed in the crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Full error message": "पूर्ण त्रुटि संदेश",
+ "@Full error message": {
+ "description": "Label for the full error message displayed in the crash details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Inbox": "इनबॉक्स",
+ "@Inbox": {
+ "description": "Title for the Inbox screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your inbox messages.": "आपके इनबॉक्स संदेशों को लोड करने में त्रुटि हुई।",
+ "@There was an error loading your inbox messages.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Subject": "कोई विषय नहीं",
+ "@No Subject": {
+ "description": "Title used for inbox messages that have no subject",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unable to fetch courses. Please check your connection and try again.": "पाठ्यक्रम प्राप्त करने में असमर्थ। कृपया अपने कनेक्शन की जांच करें और फिर से कोशिश करें।",
+ "@Unable to fetch courses. Please check your connection and try again.": {
+ "description": "Message shown when an error occured while loading courses",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Choose a course to message": "संदेश के लिए कोई पाठ्यक्रम चुनें",
+ "@Choose a course to message": {
+ "description": "Header in the course list shown when the user is choosing which course to associate with a new message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Inbox Zero": "इनबॉक्स ज़ीरो",
+ "@Inbox Zero": {
+ "description": "Title of the message shown when there are no inbox messages",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You’re all caught up!": "आपका काम पूरा हुआ!",
+ "@You’re all caught up!": {
+ "description": "Subtitle of the message shown when there are no inbox messages",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading recipients for this course": "इस पाठ्यक्रम के लिए प्राप्तकर्ता लोड करने में त्रुटि हुई",
+ "@There was an error loading recipients for this course": {
+ "description": "Message shown when attempting to create a new message but the recipients list failed to load",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unable to send message. Check your connection and try again.": "संदेश भेजने में असमर्थ। अपने कनेक्शन की जांच करें और फिर से कोशिश करें।",
+ "@Unable to send message. Check your connection and try again.": {
+ "description": "Message show when there was an error creating or sending a new message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unsaved changes": "बिना सहेजे गए बदलाव",
+ "@Unsaved changes": {
+ "description": "Title of the dialog shown when the user tries to leave with unsaved changes",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Are you sure you wish to close this page? Your unsent message will be lost.": "क्या आप सच में इस पेज को बंद करना चाहते हैं? आपका नहीं भेजा गया संदेश गुम हो जाएगा।",
+ "@Are you sure you wish to close this page? Your unsent message will be lost.": {
+ "description": "Body text of the dialog shown when the user tries leave with unsaved changes",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "New message": "नया संदेश",
+ "@New message": {
+ "description": "Title of the new-message screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Add attachment": "संलग्नक जोड़ें",
+ "@Add attachment": {
+ "description": "Tooltip for the add-attachment button in the new-message screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Send message": "संदेश भेजें",
+ "@Send message": {
+ "description": "Tooltip for the send-message button in the new-message screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Select recipients": "प्राप्तकर्ता चुनें",
+ "@Select recipients": {
+ "description": "Tooltip for the button that allows users to select message recipients",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No recipients selected": "कोई प्राप्तकर्ता नहीं चुना गया",
+ "@No recipients selected": {
+ "description": "Hint displayed when the user has not selected any message recipients",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Message subject": "संदेश विषय",
+ "@Message subject": {
+ "description": "Hint text displayed in the input field for the message subject",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Message": "संदेश",
+ "@Message": {
+ "description": "Hint text displayed in the input field for the message body",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Recipients": "प्राप्तकर्ता",
+ "@Recipients": {
+ "description": "Label for message recipients",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "plusRecipientCount": "+{count}",
+ "@plusRecipientCount": {
+ "description": "Shows the number of recipients that are selected but not displayed on screen.",
+ "type": "text",
+ "placeholders_order": [
+ "count"
+ ],
+ "placeholders": {
+ "count": {
+ "example": 5
+ }
+ }
+ },
+ "Failed. Tap for options.": "विफल हुआ। विकल्पों के लिए टैप करें।",
+ "@Failed. Tap for options.": {
+ "description": "Short message shown on a message attachment when uploading has failed",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "courseForWhom": "{studentShortName} के लिए",
+ "@courseForWhom": {
+ "description": "Describes for whom a course is for (i.e. for Bill)",
+ "type": "text",
+ "placeholders_order": [
+ "studentShortName"
+ ],
+ "placeholders": {
+ "studentShortName": {}
+ }
+ },
+ "messageLinkPostscript": "इसके संबंध में: {studentName}, {linkUrl}",
+ "@messageLinkPostscript": {
+ "description": "A postscript appended to new messages that clarifies which student is the subject of the message and also includes a URL for the related Canvas component (course, assignment, event, etc).",
+ "type": "text",
+ "placeholders_order": [
+ "studentName",
+ "linkUrl"
+ ],
+ "placeholders": {
+ "studentName": {},
+ "linkUrl": {}
+ }
+ },
+ "There was an error loading this conversation": "इस बातचीत को लोड करने में त्रुटि हुई",
+ "@There was an error loading this conversation": {
+ "description": "Message shown when a conversation fails to load",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Reply": "उत्तर दें",
+ "@Reply": {
+ "description": "Button label for replying to a conversation",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Reply All": "सभी के लिए उत्तर दें",
+ "@Reply All": {
+ "description": "Button label for replying to all conversation participants",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unknown User": "अज्ञात उपयोगकर्ता",
+ "@Unknown User": {
+ "description": "Label used where the user name is not known",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "me": "मैं",
+ "@me": {
+ "description": "First-person pronoun (i.e. 'me') that will be used in message author info, e.g. 'Me to 4 others' or 'Jon Snow to me'",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "authorToRecipient": "{authorName} से {recipientName} तक",
+ "@authorToRecipient": {
+ "description": "Author info for a single-recipient message; includes both the author name and the recipient name.",
+ "type": "text",
+ "placeholders_order": [
+ "authorName",
+ "recipientName"
+ ],
+ "placeholders": {
+ "authorName": {},
+ "recipientName": {}
+ }
+ },
+ "authorToNOthers": "{howMany,plural, =1{{authorName} से 1 अन्य}other{{authorName} से {howMany} अन्य}}",
+ "@authorToNOthers": {
+ "description": "Author info for a mutli-recipient message; includes the author name and the number of recipients",
+ "type": "text",
+ "placeholders_order": [
+ "authorName",
+ "howMany"
+ ],
+ "placeholders": {
+ "authorName": {},
+ "howMany": {}
+ }
+ },
+ "authorToRecipientAndNOthers": "{howMany,plural, =1{{authorName} से {recipientName} और 1 अन्य}other{{authorName} से {recipientName} और {howMany} अन्य}}",
+ "@authorToRecipientAndNOthers": {
+ "description": "Author info for a multi-recipient message; includes the author name, one recipient name, and the number of other recipients",
+ "type": "text",
+ "placeholders_order": [
+ "authorName",
+ "recipientName",
+ "howMany"
+ ],
+ "placeholders": {
+ "authorName": {},
+ "recipientName": {},
+ "howMany": {}
+ }
+ },
+ "Download": "डाउनलोड करें",
+ "@Download": {
+ "description": "Label for the button that will begin downloading a file",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Open with another app": "किसी अन्य ऐप से खोलें",
+ "@Open with another app": {
+ "description": "Label for the button that will allow users to open a file with another app",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There are no installed applications that can open this file": "ऐसे कोई इंस्टॉल किए गए ऐप्लिकेशन नहीं हैं जो इस फ़ाइल को खोल सकें",
+ "@There are no installed applications that can open this file": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unsupported File": "असमार्थित फ़ाइल",
+ "@Unsupported File": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "This file is unsupported and can’t be viewed through the app": "यह फ़ाइल असमर्थित है और इसे ऐप के माध्यम से नहीं देखा जा सकता है",
+ "@This file is unsupported and can’t be viewed through the app": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unable to play this media file": "इस मीडिया फ़ाइल को चलाने में असमर्थ",
+ "@Unable to play this media file": {
+ "description": "Message shown when audio or video media could not be played",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Unable to load this image": "यह छवि लोड करने में असमर्थ",
+ "@Unable to load this image": {
+ "description": "Message shown when an image file could not be loaded or displayed",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading this file": "इस फ़ाइल को लोड करने में त्रुटि हुई",
+ "@There was an error loading this file": {
+ "description": "Message shown when a file could not be loaded or displayed",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Courses": "कोई पाठ्यक्रम नहीं",
+ "@No Courses": {
+ "description": "Title for having no courses",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Your student’s courses might not be published yet.": "हो सकता है कि आपके छात्र के पाठ्यक्रम अभी तक प्रकाशित न हुए हों।",
+ "@Your student’s courses might not be published yet.": {
+ "description": "Message for having no courses",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your student’s courses.": "आपके छात्र के पाठ्यक्रम लोड करने में त्रुटि हुई।",
+ "@There was an error loading your student’s courses.": {
+ "description": "Message displayed when the list of student courses could not be loaded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Grade": "कोई ग्रेड नहीं",
+ "@No Grade": {
+ "description": "Message shown when there is currently no grade available for a course",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Filter by": "इसके अनुसार फ़िल्टर करें",
+ "@Filter by": {
+ "description": "Title for list of terms to filter grades by",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Grades": "ग्रेड",
+ "@Grades": {
+ "description": "Label for the \"Grades\" tab in course details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Syllabus": "पाठ्यक्रम",
+ "@Syllabus": {
+ "description": "Label for the \"Syllabus\" tab in course details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Front Page": "मुख पृष्ठ",
+ "@Front Page": {
+ "description": "Label for the \"Front Page\" tab in course details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Summary": "संक्षिप्त विवरण",
+ "@Summary": {
+ "description": "Label for the \"Summary\" tab in course details",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Send a message about this course": "इस पाठ्यक्रम के बारे में संदेश भेजें",
+ "@Send a message about this course": {
+ "description": "Accessibility hint for the course messaage floating action button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Total Grade": "कुल ग्रेड",
+ "@Total Grade": {
+ "description": "Label for the total grade in the course",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Graded": "ग्रेड किए गए",
+ "@Graded": {
+ "description": "Label for assignments that have been graded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Submitted": "सबमिट किया गया",
+ "@Submitted": {
+ "description": "Label for assignments that have been submitted",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Not Submitted": "सबमिट नहीं किया गया",
+ "@Not Submitted": {
+ "description": "Label for assignments that have not been submitted",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Late": "विलंब",
+ "@Late": {
+ "description": "Label for assignments that have been marked late or submitted late",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Missing": "लापता",
+ "@Missing": {
+ "description": "Label for assignments that have been marked missing or are not submitted and past the due date",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "-": "-",
+ "@-": {
+ "description": "Value representing no score for student submission",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "All Grading Periods": "सभी ग्रेडिंग अवधियां",
+ "@All Grading Periods": {
+ "description": "Label for selecting all grading periods",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Assignments": "कोई असाइनमेंट नहीं",
+ "@No Assignments": {
+ "description": "Title for the no assignments message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "It looks like assignments haven't been created in this space yet.": "लगता है इस खाली स्थान में अभी तक कोई असाइनमेंट नहीं बनाई गई है।",
+ "@It looks like assignments haven't been created in this space yet.": {
+ "description": "Message for no assignments",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading the summary details for this course.": "इस पाठ्यक्रम के लिए संक्षिप्त विवरण लोड करने में त्रुटि हुई।",
+ "@There was an error loading the summary details for this course.": {
+ "description": "Message shown when the course summary could not be loaded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Summary": "कोई संक्षिप्त विवरण नहीं",
+ "@No Summary": {
+ "description": "Title displayed when there are no items in the course summary",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "This course does not have any assignments or calendar events yet.": "इस पाठ्यक्रम में अभी तक कोई असाइनमेंट या कैलेंडर घटना नहीं है।",
+ "@This course does not have any assignments or calendar events yet.": {
+ "description": "Message displayed when there are no items in the course summary",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "gradeFormatScoreOutOfPointsPossible": "{score} / {pointsPossible}",
+ "@gradeFormatScoreOutOfPointsPossible": {
+ "description": "Formatted string for a student score out of the points possible",
+ "type": "text",
+ "placeholders_order": [
+ "score",
+ "pointsPossible"
+ ],
+ "placeholders": {
+ "score": {},
+ "pointsPossible": {}
+ }
+ },
+ "contentDescriptionScoreOutOfPointsPossible": "{pointsPossible} में से {score} पॉइंट्स",
+ "@contentDescriptionScoreOutOfPointsPossible": {
+ "description": "Formatted string for a student score out of the points possible",
+ "type": "text",
+ "placeholders_order": [
+ "score",
+ "pointsPossible"
+ ],
+ "placeholders": {
+ "score": {},
+ "pointsPossible": {}
+ }
+ },
+ "gradesSubjectMessage": "इसके संबंध में: {studentName}, ग्रेड",
+ "@gradesSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a student's grades",
+ "type": "text",
+ "placeholders_order": [
+ "studentName"
+ ],
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "syllabusSubjectMessage": "इसके संबंध में: {studentName}, पाठ्यक्रम",
+ "@syllabusSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a course syllabus",
+ "type": "text",
+ "placeholders_order": [
+ "studentName"
+ ],
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "frontPageSubjectMessage": "इसके संबंध में: {studentName}, मुख पृष्ठ",
+ "@frontPageSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a course front page",
+ "type": "text",
+ "placeholders_order": [
+ "studentName"
+ ],
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "assignmentSubjectMessage": "इसके संबंध में: {studentName}, असाइनमेंट - {assignmentName}",
+ "@assignmentSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a student's assignment",
+ "type": "text",
+ "placeholders_order": [
+ "studentName",
+ "assignmentName"
+ ],
+ "placeholders": {
+ "studentName": {},
+ "assignmentName": {}
+ }
+ },
+ "eventSubjectMessage": "इसके संबंध में: {studentName}, घटना - {eventTitle}",
+ "@eventSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a calendar event",
+ "type": "text",
+ "placeholders_order": [
+ "studentName",
+ "eventTitle"
+ ],
+ "placeholders": {
+ "studentName": {},
+ "eventTitle": {}
+ }
+ },
+ "There is no page information available.": "पेज की कोई जानकारी उपलब्ध नहीं है।",
+ "@There is no page information available.": {
+ "description": "Description for when no page information is available",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Assignment Details": "असाइनमेंट विवरण",
+ "@Assignment Details": {
+ "description": "Title for the page that shows details for an assignment",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "assignmentTotalPoints": "{points} पॉइंट्स",
+ "@assignmentTotalPoints": {
+ "description": "Label used for the total points the assignment is worth",
+ "type": "text",
+ "placeholders_order": [
+ "points"
+ ],
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "assignmentTotalPointsAccessible": "{points} पॉइंट्स",
+ "@assignmentTotalPointsAccessible": {
+ "description": "Screen reader label used for the total points the assignment is worth",
+ "type": "text",
+ "placeholders_order": [
+ "points"
+ ],
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "Due": "नियत",
+ "@Due": {
+ "description": "Label for an assignment due date",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Grade": "ग्रेड",
+ "@Grade": {
+ "description": "Label for the section that displays an assignment's grade",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Locked": "लॉक किया गया",
+ "@Locked": {
+ "description": "Label for when an assignment is locked",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "assignmentLockedModule": "यह असाइनमेंट \"{moduleName}\" मॉड्यूल द्वारा लॉक की गई है।",
+ "@assignmentLockedModule": {
+ "description": "The locked description when an assignment is locked by a module",
+ "type": "text",
+ "placeholders_order": [
+ "moduleName"
+ ],
+ "placeholders": {
+ "moduleName": {}
+ }
+ },
+ "Remind Me": "मुझे याद दिलाएं",
+ "@Remind Me": {
+ "description": "Label for the row to set reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Set a date and time to be notified of this specific assignment.": "इस विशिष्ट असाइनमेंट की सूचना पाने के लिए कोई तिथि और समय निर्धारित करें।",
+ "@Set a date and time to be notified of this specific assignment.": {
+ "description": "Description for row to set reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You will be notified about this assignment on…": "आपको इस असाइनमेंट के बारे में सूचित किया जाएगा...",
+ "@You will be notified about this assignment on…": {
+ "description": "Description for when a reminder is set",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Instructions": "निर्देश",
+ "@Instructions": {
+ "description": "Label for the description of the assignment when it has quiz instructions",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Send a message about this assignment": "इस असाइनमेंट के बारे में संदेश भेजें",
+ "@Send a message about this assignment": {
+ "description": "Accessibility hint for the assignment messaage floating action button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "This app is not authorized for use.": "यह ऐप उपयोग के लिए अधिकृत नहीं है।",
+ "@This app is not authorized for use.": {
+ "description": "The error shown when the app being used is not verified by Canvas",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The server you entered is not authorized for this app.": "आपके द्वारा दर्ज किया गया सर्वर इस ऐप के लिए अधिकृत नहीं है।",
+ "@The server you entered is not authorized for this app.": {
+ "description": "The error shown when the desired login domain is not verified by Canvas",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The user agent for this app is not authorized.": "इस ऐप के लिए उपयोगकर्ता एजेंट अधिकृत नहीं है।",
+ "@The user agent for this app is not authorized.": {
+ "description": "The error shown when the user agent during verification is not verified by Canvas",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We were unable to verify the server for use with this app.": "हम इस ऐप के उपयोग के लिए सर्वर को सत्यापित करने में असमर्थ थे।",
+ "@We were unable to verify the server for use with this app.": {
+ "description": "The generic error shown when we are unable to verify with Canvas",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Reminders": "अनुस्मारक",
+ "@Reminders": {
+ "description": "Name of the system notification channel for assignment and event reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Notifications for reminders about assignments and calendar events": "असाइनमेंट और कैलेंडर घटना के बारे में अनुस्मारक के लिए सूचनाएं",
+ "@Notifications for reminders about assignments and calendar events": {
+ "description": "Description of the system notification channel for assignment and event reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Reminders have changed!": "अनुस्मारक बदल गए हैं!",
+ "@Reminders have changed!": {
+ "description": "Title of the dialog shown when the user needs to update their reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "In order to provide you with a better experience, we have updated how reminders work. You can add new reminders by viewing an assignment or calendar event and tapping the switch under the \"Remind Me\" section. \n\nBe aware that any reminders created with older versions of this app will not be compatible with the new changes and you will need to create them again.": "आपके अनुभव को बेहतर बनाने के लिए, हमने अनुस्मारक की कार्यक्षमता को उन्नत किया है। आप किसी असाइनमेंट या कैलेंडर घटना को देखकर और \"मुझे याद दिलाएं\" अनुभाग के अंतर्गत स्विच को टैप करके नए अनुस्मारक जोड़ सकते हैं।\n\nध्यान रखें कि इस ऐप के पुराने संस्करणों के साथ बनाया गया कोई भी अनुस्मारक नए परिवर्तनों के साथ संगत नहीं होगा और आपको उन्हें फिर से बनाने की आवश्यकता होगी।",
+ "@In order to provide you with a better experience, we have updated how reminders work. You can add new reminders by viewing an assignment or calendar event and tapping the switch under the \"Remind Me\" section.\n\nBe aware that any reminders created with older versions of this app will not be compatible with the new changes and you will need to create them again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Not a parent?": "माता-पिता नहीं है?",
+ "@Not a parent?": {
+ "description": "Title for the screen that shows when the user is not observing any students",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We couldn't find any students associated with this account": "हमें इस खाते से जुड़ा कोई भी छात्र नहीं मिला",
+ "@We couldn't find any students associated with this account": {
+ "description": "Subtitle for the screen that shows when the user is not observing any students",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Are you a student or teacher?": "कआप छात्र हैं या शिक्षक हैं?",
+ "@Are you a student or teacher?": {
+ "description": "Label for button that will show users the option to view other Canvas apps in the Play Store",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "One of our other apps might be a better fit. Tap one to visit the Play Store.": "हमारा एक अन्य ऐप संभावित रूप से आपकी आवश्यकताओं को बेहतर ढंग से पूरा कर सकता है। प्ले स्टोर पर जाने के लिए टैप करें।",
+ "@One of our other apps might be a better fit. Tap one to visit the Play Store.": {
+ "description": "Description of options to view other Canvas apps in the Play Store",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Return to Login": "लॉगिन पर वापस जाएं",
+ "@Return to Login": {
+ "description": "Label for the button that returns the user to the login screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "STUDENT": "छात्र",
+ "@STUDENT": {
+ "description": "The \"student\" portion of the \"Canvas Student\" app name, in all caps. \"Canvas\" is excluded in this context as it will be displayed to the user as a wordmark image",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "TEACHER": "शिक्षक",
+ "@TEACHER": {
+ "description": "The \"teacher\" portion of the \"Canvas Teacher\" app name, in all caps. \"Canvas\" is excluded in this context as it will be displayed to the user as a wordmark image",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Canvas Student": "Canvas छात्र",
+ "@Canvas Student": {
+ "description": "The name of the Canvas Student app. Only \"Student\" should be translated as \"Canvas\" is a brand name in this context and should not be translated.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Canvas Teacher": "Canvas शिक्षक",
+ "@Canvas Teacher": {
+ "description": "The name of the Canvas Teacher app. Only \"Teacher\" should be translated as \"Canvas\" is a brand name in this context and should not be translated.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Alerts": "कोई चेतावनी नहीं",
+ "@No Alerts": {
+ "description": "The title for the empty message to show to users when there are no alerts for the student.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There’s nothing to be notified of yet.": "अभी तक सूचित करने लायक कुछ भी नहीं है।",
+ "@There’s nothing to be notified of yet.": {
+ "description": "The empty message to show to users when there are no alerts for the student.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "dismissAlertLabel": "{alertTitle} खारिज करें",
+ "@dismissAlertLabel": {
+ "description": "Accessibility label to dismiss an alert",
+ "type": "text",
+ "placeholders_order": [
+ "alertTitle"
+ ],
+ "placeholders": {
+ "alertTitle": {}
+ }
+ },
+ "Course Announcement": "पाठ्यक्रम की घोषणा",
+ "@Course Announcement": {
+ "description": "Title for alerts when there is a course announcement",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Institution Announcement": "संस्थान की घोषणा",
+ "@Institution Announcement": {
+ "description": "Title for alerts when there is an institution announcement",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "assignmentGradeAboveThreshold": "{threshold} से ऊपर का असाइनमेंट ग्रेड",
+ "@assignmentGradeAboveThreshold": {
+ "description": "Title for alerts when an assignment grade is above the threshold value",
+ "type": "text",
+ "placeholders_order": [
+ "threshold"
+ ],
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "assignmentGradeBelowThreshold": "{threshold} से नीचे का असाइनमेंट ग्रेड",
+ "@assignmentGradeBelowThreshold": {
+ "description": "Title for alerts when an assignment grade is below the threshold value",
+ "type": "text",
+ "placeholders_order": [
+ "threshold"
+ ],
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "courseGradeAboveThreshold": "{threshold} से ऊपर का पाठ्यक्रम ग्रेड",
+ "@courseGradeAboveThreshold": {
+ "description": "Title for alerts when a course grade is above the threshold value",
+ "type": "text",
+ "placeholders_order": [
+ "threshold"
+ ],
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "courseGradeBelowThreshold": "{threshold} से नीचे का पाठ्यक्रम ग्रेड",
+ "@courseGradeBelowThreshold": {
+ "description": "Title for alerts when a course grade is below the threshold value",
+ "type": "text",
+ "placeholders_order": [
+ "threshold"
+ ],
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "Settings": "सेटिंग्स",
+ "@Settings": {
+ "description": "Title for the settings screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Theme": "थीम",
+ "@Theme": {
+ "description": "Label for the light/dark theme section in the settings page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Dark Mode": "डार्क मोड",
+ "@Dark Mode": {
+ "description": "Label for the button that enables dark mode",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Light Mode": "लाइट मोड",
+ "@Light Mode": {
+ "description": "Label for the button that enables light mode",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "High Contrast Mode": "हाई कंट्रास्ट मोड",
+ "@High Contrast Mode": {
+ "description": "Label for the switch that toggles high contrast mode",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Use Dark Theme in Web Content": "वेब सामग्री में डार्क थीम का उपयोग करें",
+ "@Use Dark Theme in Web Content": {
+ "description": "Label for the switch that toggles dark mode for webviews",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Appearance": "दिखावट",
+ "@Appearance": {
+ "description": "Label for the appearance section in the settings page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Successfully submitted!": "सफलतापूर्वक सबमिट किया गया!",
+ "@Successfully submitted!": {
+ "description": "Title displayed in the grade cell for an assignment that has been submitted",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "submissionStatusSuccessSubtitle": "यह असाइनमेंट {date} को {time} बजे सबमिट की गई थी और ग्रेड होने की प्रतीक्षा कर रही है",
+ "@submissionStatusSuccessSubtitle": {
+ "description": "Subtitle displayed in the grade cell for an assignment that has been submitted and is awaiting a grade",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "time"
+ ],
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "outOfPoints": "{howMany,plural, =1{1 पॉइंट में से}other{{points} पॉइंट्स में से}}",
+ "@outOfPoints": {
+ "description": "Description for an assignment grade that has points without a current scoroe",
+ "type": "text",
+ "placeholders_order": [
+ "points",
+ "howMany"
+ ],
+ "placeholders": {
+ "points": {},
+ "howMany": {}
+ }
+ },
+ "Excused": "माफ़ किया",
+ "@Excused": {
+ "description": "Grading status for an assignment marked as excused",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Complete": "पूरा हुआ",
+ "@Complete": {
+ "description": "Grading status for an assignment marked as complete",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Incomplete": "अधूरा",
+ "@Incomplete": {
+ "description": "Grading status for an assignment marked as incomplete",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "minus": "ऋण",
+ "@minus": {
+ "description": "Screen reader-friendly replacement for the \"-\" character in letter grades like \"A-\"",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "latePenalty": "विलंब दंड (-{pointsLost})",
+ "@latePenalty": {
+ "description": "Text displayed when a late penalty has been applied to the assignment",
+ "type": "text",
+ "placeholders_order": [
+ "pointsLost"
+ ],
+ "placeholders": {
+ "pointsLost": {}
+ }
+ },
+ "finalGrade": "अंतिम ग्रेड: {grade}",
+ "@finalGrade": {
+ "description": "Text that displays the final grade of an assignment",
+ "type": "text",
+ "placeholders_order": [
+ "grade"
+ ],
+ "placeholders": {
+ "grade": {}
+ }
+ },
+ "Alert Settings": "चेतावनी की सेटिंग्स",
+ "@Alert Settings": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Alert me when…": "मुझे चेतावनी दें जब…",
+ "@Alert me when…": {
+ "description": "Header for the screen where the observer chooses the thresholds that will determine when they receive alerts (e.g. when an assignment is graded below 70%)",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Course grade below": "इससे नीचे का पाठ्यक्रम ग्रेड:",
+ "@Course grade below": {
+ "description": "Label describing the threshold for when the course grade is below a certain percentage",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Course grade above": "इससे ऊपर का पाठ्यक्रम ग्रेड: ",
+ "@Course grade above": {
+ "description": "Label describing the threshold for when the course grade is above a certain percentage",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Assignment missing": "असाइनमेंट लापता है",
+ "@Assignment missing": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Assignment grade below": "इससे नीचे का असाइनमेंट ग्रेड:",
+ "@Assignment grade below": {
+ "description": "Label describing the threshold for when an assignment is graded below a certain percentage",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Assignment grade above": "इससे ऊपर का असाइनमेंट ग्रेड:",
+ "@Assignment grade above": {
+ "description": "Label describing the threshold for when an assignment is graded above a certain percentage",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Course Announcements": "पाठ्यक्रम घोषणाएं",
+ "@Course Announcements": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Institution Announcements": "संस्थान की घोषणाएं",
+ "@Institution Announcements": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Never": "कभी नहीं",
+ "@Never": {
+ "description": "Indication that tells the user they will not receive alert notifications of a specific kind",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Grade percentage": "ग्रेड प्रतिशत",
+ "@Grade percentage": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your student's alerts.": "आपके छात्र की चेतावनियां लोड करने में त्रुटि हुई।",
+ "@There was an error loading your student's alerts.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Must be below 100": "100 से कम होना चाहिए",
+ "@Must be below 100": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "mustBeBelowN": "{percentage} से कम होना चाहिए",
+ "@mustBeBelowN": {
+ "description": "Validation error to the user that they must choose a percentage below 'n'",
+ "type": "text",
+ "placeholders_order": [
+ "percentage"
+ ],
+ "placeholders": {
+ "percentage": {
+ "example": 5
+ }
+ }
+ },
+ "mustBeAboveN": "{percentage} से ऊपर होना चाहिए",
+ "@mustBeAboveN": {
+ "description": "Validation error to the user that they must choose a percentage above 'n'",
+ "type": "text",
+ "placeholders_order": [
+ "percentage"
+ ],
+ "placeholders": {
+ "percentage": {
+ "example": 5
+ }
+ }
+ },
+ "Select Student Color": "छात्र रंग का चयन करें",
+ "@Select Student Color": {
+ "description": "Title for screen that allows users to assign a color to a specific student",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Electric, blue": "इलेक्ट्रिक, नीला",
+ "@Electric, blue": {
+ "description": "Name of the Electric (blue) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Plum, Purple": "प्लम, बैंगनी",
+ "@Plum, Purple": {
+ "description": "Name of the Plum (purple) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Barney, Fuschia": "बार्नी, रानी गुलाबी",
+ "@Barney, Fuschia": {
+ "description": "Name of the Barney (fuschia) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Raspberry, Red": "रास्पबेरी, लाल",
+ "@Raspberry, Red": {
+ "description": "Name of the Raspberry (red) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Fire, Orange": "आग, नारंगी",
+ "@Fire, Orange": {
+ "description": "Name of the Fire (orange) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Shamrock, Green": "शैमरॉक, हरा",
+ "@Shamrock, Green": {
+ "description": "Name of the Shamrock (green) color",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "An error occurred while saving your selection. Please try again.": "आपका चयन सहेजते समय त्रुटि उत्पन्न हुई। कृपया फिर से कोशिश करें।",
+ "@An error occurred while saving your selection. Please try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "changeStudentColorLabel": "{studentName} के लिए रंग बदलें",
+ "@changeStudentColorLabel": {
+ "description": "Accessibility label for the button that lets users change the color associated with a specific student",
+ "type": "text",
+ "placeholders_order": [
+ "studentName"
+ ],
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "Teacher": "शिक्षक",
+ "@Teacher": {
+ "description": "Label for the Teacher enrollment type",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Student": "छात्र",
+ "@Student": {
+ "description": "Label for the Student enrollment type",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "TA": "TA",
+ "@TA": {
+ "description": "Label for the Teaching Assistant enrollment type (also known as Teacher Aid or Education Assistant), reduced to a short acronym/initialism if appropriate.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Observer": "समीक्षक",
+ "@Observer": {
+ "description": "Label for the Observer enrollment type",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Use Camera": "कैमरे का उपयोग करें",
+ "@Use Camera": {
+ "description": "Label for the action item that lets the user capture a photo using the device camera",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Upload File": "फ़ाइल अपलोड करें",
+ "@Upload File": {
+ "description": "Label for the action item that lets the user upload a file from their device",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Choose from Gallery": "गैलरी से चुनें",
+ "@Choose from Gallery": {
+ "description": "Label for the action item that lets the user select a photo from their device gallery",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Preparing…": "तैयार किया जा रहा है…",
+ "@Preparing…": {
+ "description": "Message shown while a file is being prepared to attach to a message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Add student with…": "छात्र को इसके साथ जोड़ें...",
+ "@Add student with…": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Add Student": "छात्र जोड़ें",
+ "@Add Student": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You are not observing any students.": "आप किसी भी छात्र की समीक्षा नहीं कर रहे हैं।",
+ "@You are not observing any students.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error loading your students.": "आपके छात्रों को लोड करने में त्रुटि हुई।",
+ "@There was an error loading your students.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Pairing Code": "पेयरिंग कोड",
+ "@Pairing Code": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Students can obtain a pairing code through the Canvas website": "छात्र Canvas वेबसाइट के माध्यम से पेयरिंग कोड प्राप्त कर सकते हैं",
+ "@Students can obtain a pairing code through the Canvas website": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Enter the student pairing code provided to you. If the pairing code doesn't work, it may have expired": "आपको प्रदान किया गया छात्र पेयरिंग कोड दर्ज करें। यदि पेयरिंग कोड काम नहीं करता है, तो हो सकता है वह समाप्त हो गया हो",
+ "@Enter the student pairing code provided to you. If the pairing code doesn't work, it may have expired": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Your code is incorrect or expired.": "आपका कोड गलत है या समाप्त हो गया है।",
+ "@Your code is incorrect or expired.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Something went wrong trying to create your account, please reach out to your school for assistance.": "अपना खाता बनाने की कोशिश में कुछ गलत हो गया, कृपया सहायता के लिए अपने स्कूल से संपर्क करें।",
+ "@Something went wrong trying to create your account, please reach out to your school for assistance.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "QR Code": "QR कोड",
+ "@QR Code": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Students can create a QR code using the Canvas Student app on their mobile device": "छात्र अपने मोबाइल डिवाइस पर Canvas छात्र ऐप का उपयोग करके QR कोड बना सकते हैं",
+ "@Students can create a QR code using the Canvas Student app on their mobile device": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Add new student": "नया छात्र जोड़ें",
+ "@Add new student": {
+ "description": "Semantics label for the FAB on the Manage Students Screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Select": "चयन करें",
+ "@Select": {
+ "description": "Hint text to tell the user to choose one of two options",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I have a Canvas account": "मेरे पास Canvas खाता है",
+ "@I have a Canvas account": {
+ "description": "Option to select for users that have a canvas account",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I don't have a Canvas account": "मेरे पास Canvas खाता नहीं है",
+ "@I don't have a Canvas account": {
+ "description": "Option to select for users that don't have a canvas account",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Create Account": "खाता बनाएं",
+ "@Create Account": {
+ "description": "Button text for account creation confirmation",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Full Name": "पूरा नाम",
+ "@Full Name": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email Address": "ईमेल पता",
+ "@Email Address": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Password": "पासवर्ड",
+ "@Password": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Full Name…": "पूरा नाम…",
+ "@Full Name…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email…": "ईमेल…",
+ "@Email…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Password…": "पासवर्ड…",
+ "@Password…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Please enter full name": "कृपया पूरा नाम दर्ज करें",
+ "@Please enter full name": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Please enter an email address": "कृपया ईमेल पता दर्ज करें",
+ "@Please enter an email address": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Please enter a valid email address": "कृपया मान्य ईमेल पता दर्ज करें",
+ "@Please enter a valid email address": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Password is required": "पासवर्ड की आवश्यकता है",
+ "@Password is required": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Password must contain at least 8 characters": "पासवर्ड में कम से कम 8 कैरेक्टर होने चाहिए",
+ "@Password must contain at least 8 characters": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "qrCreateAccountTos": "'खाता बनाएं' पर टैप करके, आप {termsOfService} से सहमत होते हैं और {privacyPolicy} को स्वीकार करते हैं",
+ "@qrCreateAccountTos": {
+ "description": "The text show on the account creation screen",
+ "type": "text",
+ "placeholders_order": [
+ "termsOfService",
+ "privacyPolicy"
+ ],
+ "placeholders": {
+ "termsOfService": {},
+ "privacyPolicy": {}
+ }
+ },
+ "Terms of Service": "सेवा की शर्तें",
+ "@Terms of Service": {
+ "description": "Label for the Canvas Terms of Service agreement. This will be used in the qrCreateAccountTos text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Privacy Policy": "गोपनीयता नीति",
+ "@Privacy Policy": {
+ "description": "Label for the Canvas Privacy Policy agreement. This will be used in the qrCreateAccountTos text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "View the Privacy Policy": "गोपनीयता नीति देखें",
+ "@View the Privacy Policy": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Already have an account? ": "पहले से कोई खाता है? ",
+ "@Already have an account? ": {
+ "description": "Part of multiline text span, includes AccountSignIn1-2, in that order",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Sign In": "साइन इन करें",
+ "@Sign In": {
+ "description": "Part of multiline text span, includes AccountSignIn1-2, in that order",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Hide Password": "पासवर्ड छिपाएं",
+ "@Hide Password": {
+ "description": "content description for password hide button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Show Password": "पासवर्ड दिखाएं",
+ "@Show Password": {
+ "description": "content description for password show button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Terms of Service Link": "सेवा की शर्तें लिंक",
+ "@Terms of Service Link": {
+ "description": "content description for terms of service link",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Privacy Policy Link": "गोपनीयता नीति लिंक",
+ "@Privacy Policy Link": {
+ "description": "content description for privacy policy link",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Event": "घटना",
+ "@Event": {
+ "description": "Title for the event details screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Date": "तिथि",
+ "@Date": {
+ "description": "Label for the event date",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Location": "लोकेशन",
+ "@Location": {
+ "description": "Label for the location information",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No Location Specified": "कोई लोकेशन निर्दिष्ट नहीं",
+ "@No Location Specified": {
+ "description": "Description for events that do not have a location",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "eventTime": "{startAt} - {endAt}",
+ "@eventTime": {
+ "description": "The time the event is happening, example: \"2:00 pm - 4:00 pm\"",
+ "type": "text",
+ "placeholders_order": [
+ "startAt",
+ "endAt"
+ ],
+ "placeholders": {
+ "startAt": {},
+ "endAt": {}
+ }
+ },
+ "Set a date and time to be notified of this event.": "इस घटना की सूचना पाने के लिए कोई तिथि और समय निर्धारित करें।",
+ "@Set a date and time to be notified of this event.": {
+ "description": "Description for row to set event reminders",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You will be notified about this event on…": "आपको इस घटना के बारे में सूचित किया जाएगा...",
+ "@You will be notified about this event on…": {
+ "description": "Description for when an event reminder is set",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Share Your Love for the App": "ऐप के लिए अपना प्यार साझा करें",
+ "@Share Your Love for the App": {
+ "description": "Label for option to open the app store",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Tell us about your favorite parts of the app": "हमें ऐप के अपने पसंदीदा हिस्सों के बारे में बताएं",
+ "@Tell us about your favorite parts of the app": {
+ "description": "Description for option to open the app store",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Legal": "कानूनी",
+ "@Legal": {
+ "description": "Label for legal information option",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Privacy policy, terms of use, open source": "गोपनीयता नीति, उपयोग की शर्तें, ओपन सोर्स",
+ "@Privacy policy, terms of use, open source": {
+ "description": "Description for legal information option",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Idea for Canvas Parent App [Android]": "Canvas माता-पिता ऐप [Android] के लिए आइडिया",
+ "@Idea for Canvas Parent App [Android]": {
+ "description": "The subject for the email to request a feature",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The following information will help us better understand your idea:": "निम्नलिखित जानकारी हमें आपके आइडिया को बेहतर ढंग से समझने में मदद करेगी:",
+ "@The following information will help us better understand your idea:": {
+ "description": "The header for the users information that is attached to a feature request",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Domain:": "डोमेन:",
+ "@Domain:": {
+ "description": "The label for the Canvas domain of the logged in user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "User ID:": "उपयोगकर्ता आईडी:",
+ "@User ID:": {
+ "description": "The label for the Canvas user ID of the logged in user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email:": "ईमेल:",
+ "@Email:": {
+ "description": "The label for the eamil of the logged in user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Locale:": "स्थान:",
+ "@Locale:": {
+ "description": "The label for the locale of the logged in user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Terms of Use": "उपयोग की शर्तें",
+ "@Terms of Use": {
+ "description": "Label for the terms of use",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Canvas on GitHub": "GitHub पर Canvas",
+ "@Canvas on GitHub": {
+ "description": "Label for the button that opens the Canvas project on GitHub's website",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was a problem loading the Terms of Use": "उपयोग की शर्तों को लोड करने में समस्या आई",
+ "@There was a problem loading the Terms of Use": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Device": "डिवाइस",
+ "@Device": {
+ "description": "Label used for device manufacturer/model in the error report",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "OS Version": "OS संस्करण",
+ "@OS Version": {
+ "description": "Label used for device operating system version in the error report",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Version Number": "संस्करण संख्या",
+ "@Version Number": {
+ "description": "Label used for the app version number in the error report",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Report A Problem": "समस्या की रिपोर्ट करें",
+ "@Report A Problem": {
+ "description": "Title used for generic dialog to report problems",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Subject": "विषय",
+ "@Subject": {
+ "description": "Label used for Subject text field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "A subject is required.": "विषय की आवश्यकता है।",
+ "@A subject is required.": {
+ "description": "Error shown when the subject field is empty",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "An email address is required.": "ईमेल पता आवश्यक है।",
+ "@An email address is required.": {
+ "description": "Error shown when the email field is empty",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Description": "विवरण",
+ "@Description": {
+ "description": "Label used for Description text field",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "A description is required.": "विवरण आवश्यक है।",
+ "@A description is required.": {
+ "description": "Error shown when the description field is empty",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "How is this affecting you?": "इसका आप पर क्या प्रभाव पड़ रहा है?",
+ "@How is this affecting you?": {
+ "description": "Label used for the dropdown to select how severe the issue is",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "send": "भेजें",
+ "@send": {
+ "description": "Label used for send button when reporting a problem",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Just a casual question, comment, idea, suggestion…": "बस एक अनौपचारिक प्रश्न, टिप्पणी, आइडिया, सुझाव...",
+ "@Just a casual question, comment, idea, suggestion…": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I need some help but it's not urgent.": "मुझे कुछ मदद की ज़रूरत है, लेकिन यह अत्यावश्यक नहीं है।",
+ "@I need some help but it's not urgent.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Something's broken but I can work around it to get what I need done.": "एक समस्या है, लेकिन काम को पूरा करने के लिए कोई समाधान निकाल लिया जाएगा।",
+ "@Something's broken but I can work around it to get what I need done.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I can't get things done until I hear back from you.": "जब तक आपसे प्रतिक्रिया नहीं मिल जाती, काम पूरा नहीं किया जा सकता।",
+ "@I can't get things done until I hear back from you.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "EXTREME CRITICAL EMERGENCY!!": "अत्यधिक गंभीर आपातकालीन स्थिति!!",
+ "@EXTREME CRITICAL EMERGENCY!!": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Not Graded": "ग्रेड नहीं किया गया",
+ "@Not Graded": {
+ "description": "Description for an assignment has not been graded.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login flow: Normal": "लॉगिन प्रवाह: सामान्य",
+ "@Login flow: Normal": {
+ "description": "Description for the normal login flow",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login flow: Canvas": "लॉगिन प्रवाह: Canvas",
+ "@Login flow: Canvas": {
+ "description": "Description for the Canvas login flow",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login flow: Site Admin": "लॉगिन प्रवाह: साइट व्यवस्थापक",
+ "@Login flow: Site Admin": {
+ "description": "Description for the Site Admin login flow",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login flow: Skip mobile verify": "लॉगिन प्रवाह: मोबाइल सत्यापन छोड़ें",
+ "@Login flow: Skip mobile verify": {
+ "description": "Description for the login flow that skips domain verification for mobile",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Act As User": "उपयोगकर्ता के रूप में कार्य करें",
+ "@Act As User": {
+ "description": "Label for the button that allows the user to act (masquerade) as another user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Stop Acting as User": "उपयोगकर्ता के रूप में कार्य करना बंद करें",
+ "@Stop Acting as User": {
+ "description": "Label for the button that allows the user to stop acting (masquerading) as another user",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "actingAsUser": "आप {userName} के रूप में कार्य कर रहे हैं",
+ "@actingAsUser": {
+ "description": "Message shown while acting (masquerading) as another user",
+ "type": "text",
+ "placeholders_order": [
+ "userName"
+ ],
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "\"Act as\" is essentially logging in as this user without a password. You will be able to take any action as if you were this user, and from other users' points of views, it will be as if this user performed them. However, audit logs record that you were the one who performed the actions on behalf of this user.": "\"इस रूप में कार्य करें\" अनिवार्य रूप से बिना पासवर्ड के इस उपयोगकर्ता के रूप में लॉग इन करना है। आपके पास ऐसे कार्य करने की क्षमता होगी जैसे कि आप यह उपयोगकर्ता थे, और अन्य उपयोगकर्ताओं को ऐसा प्रतीत होगा जैसे कि इस उपयोगकर्ता ने कार्य किए हैं। हालांकि, ऑडिट लॉग रिकॉर्ड बताते हैं कि वो आप ही थे जिन्होंने इस उपयोगकर्ता की ओर से कार्रवाई की थी।",
+ "@\"Act as\" is essentially logging in as this user without a password. You will be able to take any action as if you were this user, and from other users' points of views, it will be as if this user performed them. However, audit logs record that you were the one who performed the actions on behalf of this user.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Domain": "डोमेन",
+ "@Domain": {
+ "description": "Text field hint for domain url input",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You must enter a valid domain": "आपको कोई मान्य डोमेन दर्ज करना चाहिए",
+ "@You must enter a valid domain": {
+ "description": "Message displayed for domain input error",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "User ID": "उपयोगकर्ता आईडी",
+ "@User ID": {
+ "description": "Text field hint for user ID input",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You must enter a user id": "आपको उपयोगकर्ता आईडी दर्ज करनी चाहिए",
+ "@You must enter a user id": {
+ "description": "Message displayed for user Id input error",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error trying to act as this user. Please check the Domain and User ID and try again.": "इस उपयोगकर्ता के रूप में कार्य करने का प्रयास करते समय त्रुटि हुई। कृपया डोमेन और उपयोगकर्ता आईडी की जांच करें और फिर से कोशिश करें।",
+ "@There was an error trying to act as this user. Please check the Domain and User ID and try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "endMasqueradeMessage": "आप {userName} के रूप में कार्य करना बंद कर देंगे और अपने मूल खाते में वापस आ जाएंगे।",
+ "@endMasqueradeMessage": {
+ "description": "Confirmation message displayed when the user wants to stop acting (masquerading) as another user",
+ "type": "text",
+ "placeholders_order": [
+ "userName"
+ ],
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "endMasqueradeLogoutMessage": "आप {userName} के रूप में कार्य करना बंद कर देंगे और आपको लॉग आउट कर दिया जाएगा।",
+ "@endMasqueradeLogoutMessage": {
+ "description": "Confirmation message displayed when the user wants to stop acting (masquerading) as another user and will be logged out.",
+ "type": "text",
+ "placeholders_order": [
+ "userName"
+ ],
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "How are we doing?": "हमारा कार्य कैसा चल रहा है?",
+ "@How are we doing?": {
+ "description": "Title for dialog asking user to rate the app out of 5 stars.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Don't show again": "फिर से न दिखाएं",
+ "@Don't show again": {
+ "description": "Button to prevent the rating dialog from showing again.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "What can we do better?": "हम क्या बेहतर कर सकते हैं?",
+ "@What can we do better?": {
+ "description": "Hint text for providing a comment with the rating.",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Send Feedback": "फ़ीडबैक भेजें",
+ "@Send Feedback": {
+ "description": "Button to send rating with feedback",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "ratingDialogEmailSubject": "Android - Canvas Parent {version} के लिए सुझाव",
+ "@ratingDialogEmailSubject": {
+ "description": "The subject for an email to provide feedback for CanvasParent.",
+ "type": "text",
+ "placeholders_order": [
+ "version"
+ ],
+ "placeholders": {
+ "version": {}
+ }
+ },
+ "starRating": "{position,plural, =1{{position} तारा}other{{position} तारे}}",
+ "@starRating": {
+ "description": "Accessibility label for the 1 stars to 5 stars rating",
+ "type": "text",
+ "placeholders_order": [
+ "position"
+ ],
+ "placeholders": {
+ "position": {
+ "example": 1
+ }
+ }
+ },
+ "Student Pairing": "छात्र पेयरिंग",
+ "@Student Pairing": {
+ "description": "Title for the screen where users can pair to students using a QR code",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Open Canvas Student": "Canvas Student खोलें",
+ "@Open Canvas Student": {
+ "description": "Title for QR pairing tutorial screen instructing users to open the Canvas Student app",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You'll need to open your student's Canvas Student app to continue. Go into Main Menu > Settings > Pair with Observer and scan the QR code you see there.": "जारी रखने के लिए आपको अपने छात्र का Canvas Student ऐप खोलना होगा। मुख्य मेनू > सेटिंग्स > समीक्षक के साथ पेयर करें में जाएं और वहां दिखाई देने वाले QR कोड को स्कैन करें।",
+ "@You'll need to open your student's Canvas Student app to continue. Go into Main Menu > Settings > Pair with Observer and scan the QR code you see there.": {
+ "description": "Message explaining how QR code pairing works",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Screenshot showing location of pairing QR code generation in the Canvas Student app": "Canvas Student ऐप में QR कोड जेनरेशन को पेयर करने का स्थान दिखाने वाला स्क्रीनशॉट",
+ "@Screenshot showing location of pairing QR code generation in the Canvas Student app": {
+ "description": "Content Description for qr pairing tutorial screenshot",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Expired QR Code": "समाप्त हुआ QR कोड",
+ "@Expired QR Code": {
+ "description": "Error title shown when the users scans a QR code that has expired",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The QR code you scanned may have expired. Refresh the code on the student's device and try again.": "हो सकता है कि आपके द्वारा स्कैन किए गए QR कोड का समय समाप्त हो गया हो। छात्र की डिवाइस पर कोड रीफ़्रेश करें और फिर से कोशिश करें।",
+ "@The QR code you scanned may have expired. Refresh the code on the student's device and try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "A network error occurred when adding this student. Check your connection and try again.": "इस छात्र को जोड़ते समय कोई नेटवर्क त्रुटि उत्पन्न हुई। अपने कनेक्शन की जांच करें और फिर से कोशिश करें।",
+ "@A network error occurred when adding this student. Check your connection and try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Invalid QR Code": "अमान्य QR कोड",
+ "@Invalid QR Code": {
+ "description": "Error title shown when the user scans an invalid QR code",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Incorrect Domain": "गलत डोमेन",
+ "@Incorrect Domain": {
+ "description": "Error title shown when the users scane a QR code for a student that belongs to a different domain",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The student you are trying to add belongs to a different school. Log in or create an account with that school to scan this code.": "जिस छात्र को आप जोड़ने का प्रयास कर रहे हैं वह एक अलग स्कूल से है। इस कोड को स्कैन करने के लिए लॉग इन करें या उस स्कूल में खाता बनाएं।",
+ "@The student you are trying to add belongs to a different school. Log in or create an account with that school to scan this code.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Camera Permission": "कैमरा अनुमति",
+ "@Camera Permission": {
+ "description": "Error title shown when the user wans to scan a QR code but has denied the camera permission",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "This will unpair and remove all enrollments for this student from your account.": "इस तरह से आपके खाते से इस छात्र के सभी नामांकन अनपेयर हो जाएंगे और निकाल दिए जाएंगे।",
+ "@This will unpair and remove all enrollments for this student from your account.": {
+ "description": "Confirmation message shown when the user tries to delete a student from their account",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was a problem removing this student from your account. Please check your connection and try again.": "इस छात्र को आपके खाते से निकालने में समस्या हुई। कृपया अपने कनेक्शन की जांच करें और फिर से कोशिश करें।",
+ "@There was a problem removing this student from your account. Please check your connection and try again.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Cancel": "रद्द करें",
+ "@Cancel": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "next": "अगला",
+ "@next": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "ok": "ठीक है",
+ "@ok": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Yes": "हां",
+ "@Yes": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No": "नहीं",
+ "@No": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Retry": "पुन: प्रयास करें",
+ "@Retry": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Delete": "मिटाएं",
+ "@Delete": {
+ "description": "Label used for general delete/remove actions",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Done": "हो गया",
+ "@Done": {
+ "description": "Label for general done/finished actions",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Refresh": "रीफ़्रेश करें",
+ "@Refresh": {
+ "description": "Label for button to refresh data from the web",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "View Description": "विवरण देखें",
+ "@View Description": {
+ "description": "Button to view the description for an event or assignment",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "expanded": "बढ़ाया गया",
+ "@expanded": {
+ "description": "Description for the accessibility reader for list groups that are expanded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "collapsed": "संक्षिप्त किया गया",
+ "@collapsed": {
+ "description": "Description for the accessibility reader for list groups that are expanded",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "An unexpected error occurred": "एक अप्रत्याशित त्रुटि हुई",
+ "@An unexpected error occurred": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "No description": "कोई विवरण नहीं",
+ "@No description": {
+ "description": "Message used when the assignment has no description",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Launch External Tool": "बाहरी उपकरण लॉन्च करें",
+ "@Launch External Tool": {
+ "description": "Button text added to webviews to let users open external tools in their browser",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Interactions on this page are limited by your institution.": "इस पेज पर बातचीत आपके संस्थान द्वारा सीमित हैं।",
+ "@Interactions on this page are limited by your institution.": {
+ "description": "Message describing how the webview has limited access due to an instution setting",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "dateAtTime": "{time} पर {date}",
+ "@dateAtTime": {
+ "description": "The string to format dates",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "time"
+ ],
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "dueDateAtTime": "{date} को {time} बजे नियत",
+ "@dueDateAtTime": {
+ "description": "The string to format due dates",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "time"
+ ],
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "No Due Date": "कोई नियत तिथि नहीं",
+ "@No Due Date": {
+ "description": "Label for assignments that do not have a due date",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Filter": "फ़िल्टर",
+ "@Filter": {
+ "description": "Label for buttons to filter what items are visible",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "unread": "अपठित",
+ "@unread": {
+ "description": "Label for things that are marked as unread",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "unreadCount": "{count} अपठित",
+ "@unreadCount": {
+ "description": "Formatted string for when there are a number of unread items",
+ "type": "text",
+ "placeholders_order": [
+ "count"
+ ],
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "badgeNumberPlus": "{count}+",
+ "@badgeNumberPlus": {
+ "description": "Formatted string for when too many items are being notified in a badge, generally something like: 99+",
+ "type": "text",
+ "placeholders_order": [
+ "count"
+ ],
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "There was an error loading this announcement": "इस घोषणा को लोड करने में त्रुटि हुई",
+ "@There was an error loading this announcement": {
+ "description": "Message shown when an announcement detail screen fails to load",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Network error": "नेटवर्क त्रुटि",
+ "@Network error": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Under Construction": "निर्माणाधीन",
+ "@Under Construction": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We are currently building this feature for your viewing pleasure.": "हम वर्तमान में आपके देखने के अनुभव को बेहतर बनाने के लिए इस फ़ीचर को विकसित कर रहे हैं।",
+ "@We are currently building this feature for your viewing pleasure.": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Request Login Help Button": "लॉगिन मदद बटन का अनुरोध करें",
+ "@Request Login Help Button": {
+ "description": "Accessibility hint for button that opens help dialog for a login help request",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Request Login Help": "लॉगिन मदद का अनुरोध करें",
+ "@Request Login Help": {
+ "description": "Title of help dialog for a login help request",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I'm having trouble logging in": "मुझे लॉग इन करने में समस्या हो रही है",
+ "@I'm having trouble logging in": {
+ "description": "Subject of help dialog for a login help request",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "An error occurred when trying to display this link": "इस लिंक को प्रदर्शित करने का प्रयास करते समय त्रुटि उत्पन्न हुई",
+ "@An error occurred when trying to display this link": {
+ "description": "Error message shown when a link can't be opened",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "We are unable to display this link, it may belong to an institution you currently aren't logged in to.": "हम इस लिंक को प्रदर्शित करने में असमर्थ हैं, यह किसी ऐसे संस्थान से संबंधित हो सकता है जिसमें आप वर्तमान में लॉग इन नहीं हैं।",
+ "@We are unable to display this link, it may belong to an institution you currently aren't logged in to.": {
+ "description": "Description for error page shown when clicking a link",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Link Error": "लिंक त्रुटि",
+ "@Link Error": {
+ "description": "Title for error page shown when clicking a link",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Open In Browser": "ब्राउज़र में खोलें",
+ "@Open In Browser": {
+ "description": "Text for button to open a link in the browswer",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "You'll find the QR code on the web in your account profile. Click 'QR for Mobile Login' in the list.": "आपको वेब पर अपनी खाता प्रोफ़ाइल में QR कोड मिलेगा। सूची में 'मोबाइल लॉगिन के लिए QR' पर क्लिक करें।",
+ "@You'll find the QR code on the web in your account profile. Click 'QR for Mobile Login' in the list.": {
+ "description": "Text for qr login tutorial screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Locate QR Code": "QR कोड का पता लगाएं",
+ "@Locate QR Code": {
+ "description": "Text for qr login button",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Please scan a QR code generated by Canvas": "कृपया Canvas द्वारा जेनरेट किए गए QR कोड को स्कैन करें",
+ "@Please scan a QR code generated by Canvas": {
+ "description": "Text for qr login error with incorrect qr code",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "There was an error logging in. Please generate another QR Code and try again.": "लॉग इन करने में त्रुटि हुई। कृपया कोई अन्य QR कोड जेनरेट करें और फिर से कोशिश करें।",
+ "@There was an error logging in. Please generate another QR Code and try again.": {
+ "description": "Text for qr login error",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Screenshot showing location of QR code generation in browser": "ब्राउज़र में QR कोड जेनरेशन का स्थान दिखाने वाला स्क्रीनशॉट",
+ "@Screenshot showing location of QR code generation in browser": {
+ "description": "Content Description for qr login tutorial screenshot",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "QR scanning requires camera access": "QR स्कैनिंग के लिए कैमरा एक्सेस की आवश्यकता होती है",
+ "@QR scanning requires camera access": {
+ "description": "placeholder for camera error for QR code scan",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "The linked item is no longer available": "लिंक किया गया आइटम अब उपलब्ध नहीं है",
+ "@The linked item is no longer available": {
+ "description": "error message when the alert could no be opened",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Message sent": "संदेश भेजा गया",
+ "@Message sent": {
+ "description": "confirmation message on the screen when the user succesfully sends a message",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Acceptable Use Policy": "स्वीकार्य उपयोग नीति",
+ "@Acceptable Use Policy": {
+ "description": "title for the acceptable use policy screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Submit": "सबमिट करें",
+ "@Submit": {
+ "description": "submit button title for acceptable use policy screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": "या तो आप नए उपयोगकर्ता हैं या स्वीकार्य उपयोग नीति तब से बदल गई है जब आप पिछली बार इसके लिए सहमत हुए थे। जारी रखने से पहले कृपया स्वीकार्य उपयोग नीति से सहमत हों।",
+ "@Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": {
+ "description": "acceptable use policy screen description",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I agree to the Acceptable Use Policy.": "मैं स्वीकार्य उपयोग नीति से सहमत हूं।",
+ "@I agree to the Acceptable Use Policy.": {
+ "description": "acceptable use policy switch title",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "About": "इसके बारे में",
+ "@About": {
+ "description": "Title for about menu item in settings",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "App": "ऐप",
+ "@App": {
+ "description": "Title for App field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login ID": "लॉगिन आईडी",
+ "@Login ID": {
+ "description": "Title for Login ID field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email": "ईमेल",
+ "@Email": {
+ "description": "Title for Email field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Version": "संस्करण",
+ "@Version": {
+ "description": "Title for Version field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Instructure logo": "Instructure लोगो",
+ "@Instructure logo": {
+ "description": "Semantics label for the Instructure logo on the about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/flutter_parent/lib/network/utils/analytics.dart b/apps/flutter_parent/lib/network/utils/analytics.dart
index 9254d431a0..c9eeb47f20 100644
--- a/apps/flutter_parent/lib/network/utils/analytics.dart
+++ b/apps/flutter_parent/lib/network/utils/analytics.dart
@@ -14,8 +14,8 @@
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_parent/network/api/heap_api.dart';
-import 'package:flutter_parent/utils/features_utils.dart';
import 'package:flutter_parent/utils/debug_flags.dart';
+import 'package:flutter_parent/utils/features_utils.dart';
/// Event names
/// The naming scheme for the majority of these is found in a google doc so that we can be consistent
@@ -54,6 +54,7 @@ class AnalyticsEventConstants {
static const USER_PROPERTY_BUILD_TYPE = 'build_type';
static const USER_PROPERTY_OS_VERSION = 'os_version';
static const VIEWED_OLD_REMINDER_MESSAGE = 'viewed_old_reminder_message';
+ static const SUBMISSION_AND_RUBRIC_INTERACTION = 'submission_and_rubric_interaction';
}
/// (Copied from canvas-api-2, make sure to stay in sync)
diff --git a/apps/flutter_parent/lib/router/panda_router.dart b/apps/flutter_parent/lib/router/panda_router.dart
index cf6f2d3d92..b4ea35148d 100644
--- a/apps/flutter_parent/lib/router/panda_router.dart
+++ b/apps/flutter_parent/lib/router/panda_router.dart
@@ -14,6 +14,7 @@
* along with this program. If not, see .
*/
+import 'dart:convert';
import 'dart:core';
import 'package:fluro/fluro.dart';
@@ -34,13 +35,13 @@ import 'package:flutter_parent/screens/dashboard/dashboard_screen.dart';
import 'package:flutter_parent/screens/domain_search/domain_search_screen.dart';
import 'package:flutter_parent/screens/events/event_details_screen.dart';
import 'package:flutter_parent/screens/help/help_screen.dart';
-import 'package:flutter_parent/screens/settings/legal_screen.dart';
import 'package:flutter_parent/screens/help/terms_of_use_screen.dart';
import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_screen.dart';
import 'package:flutter_parent/screens/login_landing_screen.dart';
import 'package:flutter_parent/screens/not_a_parent_screen.dart';
import 'package:flutter_parent/screens/pairing/qr_pairing_screen.dart';
import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen.dart';
+import 'package:flutter_parent/screens/settings/legal_screen.dart';
import 'package:flutter_parent/screens/settings/settings_screen.dart';
import 'package:flutter_parent/screens/splash/splash_screen.dart';
import 'package:flutter_parent/screens/web_login/web_login_screen.dart';
@@ -152,8 +153,11 @@ class PandaRouter {
static final String _simpleWebView = '/internal';
- static String simpleWebViewRoute(String url, String infoText) =>
- '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.infoText}=${Uri.encodeQueryComponent(infoText)}';
+ static String simpleWebViewRoute(String url, String infoText, bool limitWebAccess) =>
+ '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.infoText}=${Uri.encodeQueryComponent(infoText)}&${_RouterKeys.limitWebAccess}=${limitWebAccess}';
+
+ static String submissionWebViewRoute(String url, String title, Map cookies, bool limitWebAccess) =>
+ '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.title}=${Uri.encodeQueryComponent(title)}&${_RouterKeys.cookies}=${jsonEncode(cookies)}&${_RouterKeys.limitWebAccess}=${limitWebAccess}';
static String settings() => '/settings';
@@ -376,7 +380,13 @@ class PandaRouter {
static Handler _simpleWebViewHandler = Handler(handlerFunc: (BuildContext? context, Map> params) {
final url = params[_RouterKeys.url]![0];
final infoText = params[_RouterKeys.infoText]?.elementAt(0);
- return SimpleWebViewScreen(url, url, infoText: infoText == null || infoText == 'null' ? null : infoText);
+ final titleParam = params[_RouterKeys.title]?.firstOrNull;
+ final title = (titleParam == null || titleParam.isEmpty) ? url : titleParam;
+ final cookiesParam = params[_RouterKeys.cookies]?.firstOrNull;
+ final cookies = (cookiesParam == null || cookiesParam.isEmpty) ? {} : jsonDecode(cookiesParam);
+ final limitWebAccess = params[_RouterKeys.limitWebAccess]?.firstOrNull == 'true';
+ return SimpleWebViewScreen(url, title, limitWebAccess,
+ infoText: infoText == null || infoText == 'null' ? null : infoText, initialCookies: cookies);
});
static Handler _syllabusHandler = Handler(handlerFunc: (BuildContext? context, Map> params) {
@@ -461,7 +471,7 @@ class PandaRouter {
final url = await _interactor.getAuthUrl(link);
if (limitWebAccess) {
// Special case for limit webview access flag (We don't want them to be able to navigate within the webview)
- locator().pushRoute(context, simpleWebViewRoute(url, L10n(context).webAccessLimitedMessage));
+ locator().pushRoute(context, simpleWebViewRoute(url, L10n(context).webAccessLimitedMessage, true));
} else if (await locator().canLaunch(link) ?? false) {
// No native route found, let's launch the url if possible, or show an error toast
locator().launch(url);
@@ -519,6 +529,9 @@ class _RouterKeys {
static final accountName = 'accountName';
static final eventId = 'eventId';
static final infoText = 'infoText';
+ static final title = 'title';
+ static final cookies = 'cookies';
+ static final limitWebAccess = 'limitWebAccess';
static final isCreatingAccount = 'isCreatingAccount';
static final loginFlow = 'loginFlow';
static final qrLoginUrl = 'qrLoginUrl';
diff --git a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart
index 2814bad688..24786721a6 100644
--- a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart
+++ b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart
@@ -17,12 +17,14 @@ import 'package:flutter_parent/models/assignment.dart';
import 'package:flutter_parent/models/reminder.dart';
import 'package:flutter_parent/models/user.dart';
import 'package:flutter_parent/network/utils/api_prefs.dart';
+import 'package:flutter_parent/router/panda_router.dart';
import 'package:flutter_parent/screens/assignments/assignment_details_interactor.dart';
import 'package:flutter_parent/screens/assignments/grade_cell.dart';
import 'package:flutter_parent/screens/inbox/create_conversation/create_conversation_screen.dart';
import 'package:flutter_parent/utils/common_widgets/error_panda_widget.dart';
import 'package:flutter_parent/utils/common_widgets/loading_indicator.dart';
import 'package:flutter_parent/utils/common_widgets/web_view/html_description_tile.dart';
+import 'package:flutter_parent/utils/common_widgets/web_view/web_content_interactor.dart';
import 'package:flutter_parent/utils/core_extensions/date_time_extensions.dart';
import 'package:flutter_parent/utils/design/canvas_icons_solid.dart';
import 'package:flutter_parent/utils/design/parent_theme.dart';
@@ -33,6 +35,7 @@ import 'package:flutter_parent/utils/service_locator.dart';
import 'package:flutter_svg/svg.dart';
import 'package:permission_handler/permission_handler.dart';
+import '../../network/utils/analytics.dart';
import '../../utils/veneers/flutter_snackbar_veneer.dart';
class AssignmentDetailsScreen extends StatefulWidget {
@@ -183,6 +186,30 @@ class _AssignmentDetailsScreenState extends State {
],
),
),
+ Divider(),
+ Padding(
+ padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
+ child: OutlinedButton(
+ onPressed: () {
+ _onSubmissionAndRubricClicked(assignment.htmlUrl, l10n.submission);
+ },
+ child: Align(
+ alignment: Alignment.center,
+ child: Row(mainAxisSize: MainAxisSize.min, children: [
+ Text(
+ l10n.submissionAndRubric,
+ style: textTheme.titleMedium?.copyWith(color: ParentTheme.of(context)?.studentColor),
+ ),
+ SizedBox(width: 6),
+ Icon(CanvasIconsSolid.arrow_open_right, color: ParentTheme.of(context)?.studentColor, size: 14)
+ ])),
+ style: OutlinedButton.styleFrom(
+ minimumSize: Size(double.infinity, 48),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(6.0),
+ ),
+ side: BorderSide(width: 0.5, color: ParentTheme.of(context)?.onSurfaceColor ?? Colors.grey),
+ ))),
if (!fullyLocked) ...[
Divider(),
..._rowTile(
@@ -372,4 +399,17 @@ class _AssignmentDetailsScreenState extends State {
locator.get().push(context, screen);
}
}
+
+ _onSubmissionAndRubricClicked(String? assignmentUrl, String title) async {
+ if (assignmentUrl == null) return;
+ final parentId = ApiPrefs.getUser()?.id ?? 0;
+ final currentStudentId = _currentStudent?.id ?? 0;
+ locator().pushRoute(context, PandaRouter.submissionWebViewRoute(
+ await locator().getAuthUrl(assignmentUrl),
+ title,
+ {"k5_observed_user_for_$parentId": "$currentStudentId"},
+ false
+ ));
+ locator().logEvent(AnalyticsEventConstants.SUBMISSION_AND_RUBRIC_INTERACTION);
+ }
}
diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart
index cc186be9dc..dcb7c6ba10 100644
--- a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart
+++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart
@@ -19,12 +19,24 @@ import 'package:flutter_parent/utils/design/parent_theme.dart';
import 'package:flutter_parent/utils/web_view_utils.dart';
import 'package:webview_flutter/webview_flutter.dart';
+import '../../service_locator.dart';
+import '../../url_launcher.dart';
+
class SimpleWebViewScreen extends StatefulWidget {
- final String _url;
- final String _title;
- final String? _infoText;
+ final String url;
+ final String title;
+ final String? infoText;
+ final Map? initialCookies;
+ final bool limitWebAccess;
- SimpleWebViewScreen(this._url, this._title, {String? infoText}) : _infoText = infoText;
+ SimpleWebViewScreen(
+ this.url,
+ this.title,
+ this.limitWebAccess, {
+ String? infoText,
+ Map? initialCookies,
+ }) : this.infoText = infoText,
+ this.initialCookies = initialCookies;
@override
State createState() => _SimpleWebViewScreenState();
@@ -43,7 +55,7 @@ class _SimpleWebViewScreenState extends State {
backgroundColor: Colors.transparent,
iconTheme: Theme.of(context).iconTheme,
bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false),
- title: Text(widget._title, style: Theme.of(context).textTheme.titleLarge),
+ title: Text(widget.title, style: Theme.of(context).textTheme.titleLarge),
),
body: WebView(
javascriptMode: JavascriptMode.unrestricted,
@@ -52,34 +64,51 @@ class _SimpleWebViewScreenState extends State {
navigationDelegate: _handleNavigation,
onWebViewCreated: (controller) {
_controller = controller;
- controller.loadUrl(widget._url);
+ controller.loadUrl(widget.url);
},
onPageFinished: _handlePageLoaded,
+ initialCookies: _getCookies(),
),
),
);
}
NavigationDecision _handleNavigation(NavigationRequest request) {
- if (!request.isForMainFrame || widget._url.startsWith(request.url)) return NavigationDecision.navigate;
+ if (request.url.contains('/download?download_frd=')) {
+ locator().launch(request.url);
+ return NavigationDecision.prevent;
+ }
+ if (!request.isForMainFrame || widget.url.startsWith(request.url) || !widget.limitWebAccess) return NavigationDecision.navigate;
return NavigationDecision.prevent;
}
void _handlePageLoaded(String url) async {
// If there's no info to show, just return
- if (widget._infoText == null || widget._infoText!.isEmpty) return;
+ if (widget.infoText == null || widget.infoText!.isEmpty) return;
// Run javascript to show the info alert
await _controller?.evaluateJavascript(_showAlertJavascript);
}
+ String _getDomain() {
+ final uri = Uri.parse(widget.url);
+ return uri.host;
+ }
+
+ List _getCookies() {
+ return widget.initialCookies?.entries
+ .map((entry) => WebViewCookie(name: entry.key.toString(), value: entry.value.toString(), domain: _getDomain()))
+ .toList() ??
+ [];
+ }
+
String get _showAlertJavascript => """
const floatNode = `
- ${widget._infoText}
+ ${widget.infoText}
diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/web_content_interactor.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/web_content_interactor.dart
index 93aaec8122..58c39f237c 100644
--- a/apps/flutter_parent/lib/utils/common_widgets/web_view/web_content_interactor.dart
+++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/web_content_interactor.dart
@@ -28,7 +28,8 @@ class WebContentInteractor {
}
Future getAuthUrl(String targetUrl) async {
- if (targetUrl.contains(ApiPrefs.getDomain()!)) {
+ final domain = ApiPrefs.getDomain();
+ if (domain != null && targetUrl.contains(domain)) {
return _authUrl(targetUrl);
} else {
return targetUrl;
diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml
index 0c370d64e5..9f1ace5fff 100644
--- a/apps/flutter_parent/pubspec.yaml
+++ b/apps/flutter_parent/pubspec.yaml
@@ -25,7 +25,7 @@ description: Canvas Parent
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 3.10.0+50
+version: 3.11.0+51
module:
androidX: true
diff --git a/apps/flutter_parent/test/router/panda_router_test.dart b/apps/flutter_parent/test/router/panda_router_test.dart
index 56b87aa149..ad6cf2c602 100644
--- a/apps/flutter_parent/test/router/panda_router_test.dart
+++ b/apps/flutter_parent/test/router/panda_router_test.dart
@@ -25,20 +25,19 @@ import 'package:flutter_parent/screens/announcements/announcement_details_screen
import 'package:flutter_parent/screens/assignments/assignment_details_screen.dart';
import 'package:flutter_parent/screens/calendar/calendar_screen.dart';
import 'package:flutter_parent/screens/calendar/calendar_widget/calendar_widget.dart';
-import 'package:flutter_parent/screens/courses/details/course_details_interactor.dart';
import 'package:flutter_parent/screens/courses/details/course_details_screen.dart';
import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shell_screen.dart';
import 'package:flutter_parent/screens/dashboard/dashboard_screen.dart';
import 'package:flutter_parent/screens/domain_search/domain_search_screen.dart';
import 'package:flutter_parent/screens/events/event_details_screen.dart';
import 'package:flutter_parent/screens/help/help_screen.dart';
-import 'package:flutter_parent/screens/settings/legal_screen.dart';
import 'package:flutter_parent/screens/help/terms_of_use_screen.dart';
import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_screen.dart';
import 'package:flutter_parent/screens/login_landing_screen.dart';
import 'package:flutter_parent/screens/not_a_parent_screen.dart';
import 'package:flutter_parent/screens/pairing/qr_pairing_screen.dart';
import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen.dart';
+import 'package:flutter_parent/screens/settings/legal_screen.dart';
import 'package:flutter_parent/screens/settings/settings_screen.dart';
import 'package:flutter_parent/screens/splash/splash_screen.dart';
import 'package:flutter_parent/screens/web_login/web_login_screen.dart';
@@ -55,7 +54,6 @@ import '../utils/accessibility_utils.dart';
import '../utils/canvas_model_utils.dart';
import '../utils/platform_config.dart';
import '../utils/test_app.dart';
-import '../utils/test_helpers/mock_helpers.dart';
import '../utils/test_helpers/mock_helpers.mocks.dart';
final _analytics = MockAnalytics();
@@ -369,6 +367,19 @@ void main() {
expect((widget as CourseRoutingShellScreen).courseId, courseId);
expect((widget).type, CourseShellType.frontPage);
});
+
+ test('submissionWebViewRoute returns SimpleWebViewScreen', () {
+ final url = 'https://test.instructure.com';
+ final title = 'Title';
+ final cookies = {'key': 'value'};
+ final widget = _getWidgetFromRoute(PandaRouter.submissionWebViewRoute(url, title, cookies, false)) as SimpleWebViewScreen;
+
+ expect(widget, isA());
+ expect(widget.url, url);
+ expect(widget.title, title);
+ expect(widget.initialCookies, cookies);
+ expect(widget.limitWebAccess, false);
+ });
});
group('external url handler', () {
@@ -584,7 +595,7 @@ void main() {
);
verify(_analytics.logMessage('Attempting to route INTERNAL url: $url')).called(1);
- verify(_mockNav.pushRoute(any, PandaRouter.simpleWebViewRoute(url, AppLocalizations().webAccessLimitedMessage)));
+ verify(_mockNav.pushRoute(any, PandaRouter.simpleWebViewRoute(url, AppLocalizations().webAccessLimitedMessage, true)));
});
});
diff --git a/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart b/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart
index ac2f283561..95b5f99721 100644
--- a/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart
+++ b/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart
@@ -33,6 +33,7 @@ import 'package:flutter_parent/screens/inbox/create_conversation/create_conversa
import 'package:flutter_parent/utils/common_widgets/error_panda_widget.dart';
import 'package:flutter_parent/utils/common_widgets/loading_indicator.dart';
import 'package:flutter_parent/utils/common_widgets/web_view/html_description_tile.dart';
+import 'package:flutter_parent/utils/common_widgets/web_view/simple_web_view_screen.dart';
import 'package:flutter_parent/utils/common_widgets/web_view/web_content_interactor.dart';
import 'package:flutter_parent/utils/core_extensions/date_time_extensions.dart';
import 'package:flutter_parent/utils/design/canvas_icons_solid.dart';
@@ -48,7 +49,6 @@ import 'package:permission_handler/permission_handler.dart';
import '../../utils/accessibility_utils.dart';
import '../../utils/platform_config.dart';
import '../../utils/test_app.dart';
-import '../../utils/test_helpers/mock_helpers.dart';
import '../../utils/test_helpers/mock_helpers.mocks.dart';
void main() {
@@ -621,4 +621,45 @@ void main() {
expect(find.text(AppLocalizations().assignmentRemindMeDescription), findsOneWidget);
expect((tester.widget(find.byType(Switch)) as Switch).value, false);
});
+
+ testWidgetsWithAccessibilityChecks('shows Submission & Rubric button', (tester) async {
+ when(interactor.loadAssignmentDetails(any, courseId, assignmentId, studentId)).thenAnswer((_) async => AssignmentDetails(assignment: assignment));
+
+ await tester.pumpWidget(TestApp(
+ AssignmentDetailsScreen(
+ courseId: courseId,
+ assignmentId: assignmentId,
+ ),
+ platformConfig: PlatformConfig(mockApiPrefs: {ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(student))}),
+ ));
+
+ // Pump for a duration since we're delaying webview load for the animation
+ await tester.pumpAndSettle(Duration(seconds: 1));
+
+ expect(find.text(AppLocalizations().submissionAndRubric), findsOneWidget);
+ });
+
+ testWidgetsWithAccessibilityChecks('Submission & Rubric button opens SimpleWebViewScreen', (tester) async {
+ when(interactor.loadAssignmentDetails(any, courseId, assignmentId, studentId)).thenAnswer((_) async => AssignmentDetails(assignment: assignment));
+
+ await tester.pumpWidget(TestApp(
+ AssignmentDetailsScreen(
+ courseId: courseId,
+ assignmentId: assignmentId,
+ ),
+ platformConfig: PlatformConfig(mockApiPrefs: {ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(student))}, initWebview: true),
+ ));
+
+ // Pump for a duration since we're delaying webview load for the animation
+ await tester.pumpAndSettle(Duration(seconds: 1));
+
+ await tester.tap(find.text(AppLocalizations().submissionAndRubric));
+ await tester.pumpAndSettle();
+
+ // Check to make sure we're on the SimpleWebViewScreen screen
+ expect(find.byType(SimpleWebViewScreen), findsOneWidget);
+
+ // Check that we have the correct title
+ expect(find.text(AppLocalizations().submission), findsOneWidget);
+ });
}
diff --git a/apps/flutter_parent/test/utils/widgets/webview/simple_web_view_screen_test.dart b/apps/flutter_parent/test/utils/widgets/webview/simple_web_view_screen_test.dart
index 944e670e5b..211e087f4b 100644
--- a/apps/flutter_parent/test/utils/widgets/webview/simple_web_view_screen_test.dart
+++ b/apps/flutter_parent/test/utils/widgets/webview/simple_web_view_screen_test.dart
@@ -29,7 +29,7 @@ void main() {
final url = 'https://www.google.com';
final title = 'title';
- await tester.pumpWidget(TestApp(SimpleWebViewScreen(url, title), platformConfig: config));
+ await tester.pumpWidget(TestApp(SimpleWebViewScreen(url, title, true), platformConfig: config));
await tester.pump();
expect(find.text(title), findsOneWidget);
@@ -39,7 +39,7 @@ void main() {
final url = 'https://www.google.com';
final title = 'title';
- await tester.pumpWidget(TestApp(SimpleWebViewScreen(url, title), platformConfig: config));
+ await tester.pumpWidget(TestApp(SimpleWebViewScreen(url, title, true), platformConfig: config));
await tester.pump();
expect(find.byType(WebView), findsOneWidget);
@@ -51,7 +51,7 @@ void main() {
final title = 'title';
await TestApp.showWidgetFromTap(tester, (context) {
- return Navigator.of(context).push(MaterialPageRoute(builder: (context) => SimpleWebViewScreen(url, title)));
+ return Navigator.of(context).push(MaterialPageRoute(builder: (context) => SimpleWebViewScreen(url, title, true)));
}, config: config);
expect(find.byType(WebView), findsOneWidget);
diff --git a/apps/flutter_sdk_url b/apps/flutter_sdk_url
deleted file mode 100644
index 6fcfede9b9..0000000000
--- a/apps/flutter_sdk_url
+++ /dev/null
@@ -1 +0,0 @@
-https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_2.5.3-stable.tar.xz
\ No newline at end of file
diff --git a/apps/gradle.properties b/apps/gradle.properties
index 582f323a7e..23b8fadbcb 100644
--- a/apps/gradle.properties
+++ b/apps/gradle.properties
@@ -1,8 +1,3 @@
android.enableJetifier=true
android.useAndroidX=true
-org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
-
-# Flutter embed
-target-platform=android-x64,android-arm,android-arm64
-flutter.hostAppProjectName=:student
-org.gradle.workers.max=1
+org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
\ No newline at end of file
diff --git a/apps/parent/.gitignore b/apps/parent/.gitignore
new file mode 100644
index 0000000000..b6df9f40c4
--- /dev/null
+++ b/apps/parent/.gitignore
@@ -0,0 +1,58 @@
+# Private data
+google-services.json
+src/main/res/xml/analytics.xml
+src/main/res/values/private_strings.xml
+private.properties
+
+# built application files #
+*.apk
+*.ap_
+
+# files for the dex VM #
+*.dex
+
+# Java class files #
+*.class
+
+# generated files #
+bin
+gen
+target
+
+# Local configuration file (sdk path, etc) #
+local.properties
+
+# Windows thumbnail db #
+Thumbs.db
+
+# OSX files
+.DS_Store
+
+# Eclipse project files #
+.classpath
+.project
+
+# Android Studio #
+.idea
+*.iml
+build
+*.iws
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# Project Files #
+.gitconfig
+
+# Gradle Files #
+gradle.properties
+.gradle
+
+# OSS License data
+src/main/res/raw/third_party_license_metadata
+src/main/res/raw/third_party_licenses
+
+# Flank
+results/
diff --git a/apps/parent/build.gradle b/apps/parent/build.gradle
new file mode 100644
index 0000000000..6f22fb2773
--- /dev/null
+++ b/apps/parent/build.gradle
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'com.google.gms.google-services'
+ id 'org.jetbrains.kotlin.android'
+ id 'kotlin-kapt'
+ id 'com.google.firebase.crashlytics'
+ id 'dagger.hilt.android.plugin'
+}
+
+configurations {
+ all*.exclude group: 'commons-logging', module: 'commons-logging'
+ all*.exclude group: 'org.apache.httpcomponents', module: 'httpclient'
+}
+
+def coverageEnabled = project.hasProperty('coverage')
+
+android {
+ namespace 'com.instructure.parentapp'
+ compileSdk Versions.COMPILE_SDK
+
+ defaultConfig {
+ applicationId "com.instructure.parentapp"
+ minSdkVersion Versions.MIN_SDK
+ targetSdkVersion Versions.TARGET_SDK
+ versionCode 50
+ versionName "3.10.0"
+
+ buildConfigField "boolean", "IS_TESTING", "false"
+ testInstrumentationRunner 'com.instructure.parentapp.ui.espresso.ParentHiltTestRunner'
+ testInstrumentationRunnerArguments disableAnalytics: 'true'
+
+ /* Add private data */
+ PrivateData.merge(project, "parent")
+ }
+
+ packagingOptions {
+ exclude 'META-INF/maven/com.google.guava/guava/pom.xml'
+ exclude 'META-INF/maven/com.google.guava/guava/pom.properties'
+ exclude 'META-INF/DEPENDENCIES'
+ exclude 'META-INF/LICENSE'
+ exclude 'META-INF/LICENSE.txt'
+ exclude 'META-INF/NOTICE'
+ exclude 'META-INF/rxjava.properties'
+ exclude 'LICENSE.txt'
+ }
+
+
+ lintOptions {
+ abortOnError false
+ }
+
+ flavorDimensions 'default'
+
+ productFlavors {
+ dev {
+ dimension 'default'
+ }
+
+ qa {
+ buildConfigField "boolean", "IS_TESTING", "true"
+ dimension 'default'
+ }
+
+ prod {
+ dimension 'default'
+ }
+ }
+
+ buildTypes {
+ debug {
+ testCoverageEnabled = coverageEnabled
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ minifyEnabled false
+ shrinkResources false
+ pseudoLocalesEnabled true
+
+ firebaseCrashlytics {
+ // If you don't need crash reporting for your debug build,
+ // you can speed up your build by disabling mapping file uploading.
+ mappingFileUploadEnabled false
+ }
+ }
+
+ debugMinify {
+ initWith debug
+ debuggable false
+ minifyEnabled true
+ shrinkResources true
+ matchingFallbacks = ['debug']
+ }
+
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ configurations.all {
+ resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9'
+ /*
+ Resolves dependency versions across test and production APKs, specifically, transitive
+ dependencies. This is required since Espresso internally has a dependency on support-annotations.
+ https://github.com/googlecodelabs/android-testing/blob/57852eaf7df88ddaf828eca879a407f2249d5348/app/build.gradle#L86
+ */
+ resolutionStrategy.force Libs.ANDROIDX_ANNOTATION
+
+ resolutionStrategy.force Libs.KOTLIN_COROUTINES_CORE
+ resolutionStrategy.force Libs.KOTLIN_STD_LIB
+ }
+
+ configurations.implementation.dependencies.each { compileDependency ->
+ println "Excluding compile dependency: ${compileDependency.getName()}"
+ configurations.androidTestImplementation.dependencies.each { androidTestCompileDependency ->
+ configurations.androidTestImplementation.exclude module: "${compileDependency.getName()}"
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ viewBinding true
+ dataBinding true
+ compose true
+ }
+ hilt {
+ enableAggregatingTask = false
+ enableExperimentalClasspathAggregation = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.KOTLIN_COMPOSE_COMPILER_VERSION
+ }
+
+ testOptions.animationsDisabled = true
+}
+
+dependencies {
+
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ /* Project Modules */
+ implementation project(path: ':login-api-2')
+
+ /* Android Test Dependencies */
+ androidTestImplementation project(path: ':espresso')
+ androidTestImplementation project(':dataseedingapi')
+
+ /* Unit Test Dependencies */
+ testImplementation Libs.JUNIT
+ testImplementation Libs.ROBOLECTRIC
+ testImplementation Libs.ANDROIDX_TEST_JUNIT
+ testImplementation Libs.MOCKK
+ androidTestImplementation Libs.ANDROIDX_TEST_JUNIT
+ testImplementation Libs.KOTLIN_COROUTINES_TEST
+ testImplementation Libs.THREETEN_BP
+ testImplementation Libs.ANDROIDX_CORE_TESTING
+
+ /* Firebase */
+ implementation platform(Libs.FIREBASE_BOM) {
+ exclude group: 'com.google.firebase', module: 'firebase-analytics'
+ }
+ implementation Libs.FIREBASE_MESSAGING
+ implementation Libs.FIREBASE_CRASHLYTICS_NDK
+ implementation(Libs.FIREBASE_CRASHLYTICS) {
+ transitive = true
+ }
+
+ /* Kotlin */
+ implementation Libs.KOTLIN_STD_LIB
+
+ /* DI */
+ implementation Libs.HILT
+ kapt Libs.HILT_COMPILER
+ implementation Libs.HILT_ANDROIDX_WORK
+ kapt Libs.HILT_ANDROIDX_COMPILER
+ androidTestImplementation Libs.HILT_TESTING
+
+ /* ROOM */
+ implementation Libs.ROOM
+ kapt Libs.ROOM_COMPILER
+ implementation Libs.ROOM_COROUTINES
+
+ /* Navigation */
+ implementation Libs.NAVIGATION_FRAGMENT
+ implementation Libs.NAVIGATION_KTX
+
+ /* Support dependencies */
+ implementation Libs.ANDROIDX_ANNOTATION
+ implementation Libs.ANDROIDX_APPCOMPAT
+ implementation Libs.ANDROIDX_BROWSER
+ implementation Libs.ANDROIDX_CARDVIEW
+ implementation Libs.ANDROIDX_CONSTRAINT_LAYOUT
+ implementation Libs.ANDROIDX_DESIGN
+ implementation Libs.ANDROIDX_RECYCLERVIEW
+ implementation Libs.ANDROIDX_PALETTE
+ implementation Libs.PLAY_IN_APP_UPDATES
+
+ androidTestImplementation Libs.COMPOSE_UI_TEST
+}
\ No newline at end of file
diff --git a/apps/parent/proguard-rules.txt b/apps/parent/proguard-rules.txt
new file mode 100644
index 0000000000..925fea2093
--- /dev/null
+++ b/apps/parent/proguard-rules.txt
@@ -0,0 +1,250 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+-dontoptimize
+-verbose
+
+-dontpreverify
+-optimizations !code/simplification/arithmetic
+
+-keepattributes *Annotation*,Signature,EnclosingMethod,ElementList,Root,Annotation,InnerClasses,SourceFile,LineNumberTable
+
+# Android
+
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+-keep public class * implements java.io.Serializable
+-keep public class * extends androidx.fragment.app.Fragment
+-keep public class * extends androidx.fragment.app.ListFragment
+-keep public class * extends android.os.AsyncTask
+
+-keep public class * extends android.view.View {
+ public (android.content.Context);
+ public (android.content.Context, android.util.AttributeSet);
+ public (android.content.Context, android.util.AttributeSet, int);
+ public void set*(...);
+}
+
+-keepclasseswithmembers class * {
+ public (android.content.Context, android.util.AttributeSet);
+}
+
+-keepclasseswithmembers class * {
+ public (android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers class * extends android.content.Context {
+ public void *(android.view.View);
+ public void *(android.view.MenuItem);
+}
+
+-keepclassmembers class * implements android.os.Parcelable {
+ static ** CREATOR;
+}
+
+-keepnames class * implements android.os.Parcelable {
+ public static final ** CREATOR;
+}
+
+-keepclassmembers class **.R$* {
+ public static ;
+}
+
+-keepclassmembers class * {
+ @android.webkit.JavascriptInterface ;
+}
+
+-keepclassmembers enum * {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ private static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
+
+-keep class * extends java.util.ListResourceBundle {
+ protected Object[][] getContents();
+}
+
+-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
+ public static final *** NULL;
+}
+
+-keepnames @com.google.android.gms.common.annotation.KeepName class *
+-keepclassmembernames class * {
+ @com.google.android.gms.common.annotation.KeepName *;
+}
+-dontwarn com.google.android.gms.**
+
+-dontwarn com.google.api.**
+-dontwarn android.net.http.**
+-keep class com.google.api.** { *; }
+-keep class android.net.http.** { *; }
+
+# Google
+
+-keep class com.google.appengine.** { *; }
+
+# Apache
+
+-dontwarn org.apache.**
+-keep class org.apache.http.** { *; }
+-keep class org.apache.james.mime4j.** { *; }
+
+# Amazon
+
+-keep class com.amazonaws.services.sqs.QueueUrlHandler { *; }
+-keep class com.amazonaws.javax.xml.transform.sax.* { public *; }
+-keep class com.amazonaws.javax.xml.stream.** { *; }
+-keep class com.amazonaws.services.**.model.*Exception* { *; }
+-keep class com.amazonaws.internal.** { *; }
+-keep class org.codehaus.** { *; }
+-keep class org.joda.convert.* { *; }
+-keepnames class com.fasterxml.jackson.** { *; }
+-keepnames class com.amazonaws.** { *; }
+
+-dontwarn com.amazonaws.auth.policy.conditions.S3ConditionFactory
+-dontwarn org.joda.time.**
+-dontwarn com.fasterxml.jackson.databind.**
+-dontwarn javax.xml.stream.events.**
+-dontwarn org.codehaus.jackson.**
+-dontwarn org.apache.commons.logging.impl.**
+-dontwarn org.apache.http.conn.scheme.**
+-dontwarn org.apache.http.annotation.**
+-dontwarn org.ietf.jgss.**
+-dontwarn org.w3c.dom.bootstrap.**
+
+-keep class org.ietf.** { *; }
+-keep interface org.ietf.** { *; }
+
+-dontwarn com.amazon.**
+-dontwarn com.amazonaws.**
+
+-keep class com.amazon.** { *; }
+-dontwarn org.apache.http.annotation.**
+-keep class com.amazonaws.** { *; }
+-keep class org.apache.commons.logging.** { *; }
+-keep class com.amazonaws.services.sqs.QueueUrlHandler { *; }
+-keep class com.amazonaws.javax.xml.transform.sax.* { public *; }
+-keep class com.amazonaws.javax.xml.stream.** { *; }
+-keep class com.amazonaws.services.**.model.*Exception* { *; }
+-keep class org.codehaus.** { *; }
+
+-dontwarn javax.xml.stream.events.**
+-dontwarn org.codehaus.jackson.**
+-dontwarn org.apache.commons.logging.impl.**
+-dontwarn org.apache.http.conn.scheme.**
+
+-keep class android.webkit.** { *; }
+
+# Retrofit
+
+-dontwarn retrofit2.**
+-dontwarn retrofit.**
+-dontwarn rx.**
+-dontwarn okio.**
+-dontwarn com.squareup.okhttp.*
+-dontwarn retrofit.appengine.UrlFetchClient
+-keep class com.google.gson.** { *; }
+-keep class com.google.inject.** { *; }
+-keep class javax.inject.** { *; }
+-keep class retrofit2.** { *; }
+-keep class retrofit.** { *; }
+-keepclasseswithmembers class * {
+ @retrofit.http.* ;
+}
+
+# Retrofit, OkHttp, Gson
+
+-keep class com.squareup.okhttp.** { *; }
+-keep interface com.squareup.okhttp.** { *; }
+-dontwarn com.squareup.okhttp.**
+-keep class sun.misc.Unsafe { *; }
+-dontwarn java.nio.file.*
+-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+-dontwarn okhttp3.**
+-keep class okhttp3.** { *; }
+
+# PSPDFKit
+-keep class com.pspdfkit.** { *; }
+-dontwarn com.pspdfkit.**
+-dontwarn sun.misc.**
+-dontwarn android.view.WindowInsets
+-dontwarn android.graphics.Insets
+-dontwarn edu.umd.cs.findbugs.annotations.SuppressWarnings
+-keepnames class io.reactivex.android.schedulers.AndroidSchedulers
+-keepnames class io.reactivex.Observable
+
+# RxJava
+-keep class rx.internal.util.unsafe.** { *; }
+
+# SimpleFramework.xml
+-dontwarn org.simpleframework.xml.stream.**
+-keep public class org.simpleframework.** { *; }
+-keep class org.simpleframework.xml.** { *; }
+-keep class org.simpleframework.xml.core.** { *; }
+-keep class org.simpleframework.xml.util.** { *; }
+-keep class javax.xml.stream.** { *; }
+
+-keepclassmembers class * {
+ @org.simpleframework.xml.* *;
+}
+
+# Instructure
+
+-keep public class com.instructure.** { *; }
+-keep public class instructure.** { *; }
+
+# androidsvg
+
+-dontwarn com.caverock.androidsvg.**
+-keep public class com.caverock.androidsvg.** { *; }
+
+# Greenrobot event bus
+-keep public class org.greenrobot.eventbus.** { *; }
+
+# Glide
+-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
+-keep public class * extends com.bumptech.glide.AppGlideModule
+-keep public class * implements com.bumptech.glide.module.GlideModule
+-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
+ **[] $VALUES;
+ public *;
+}
+
+# afollestad material dialogs
+-dontwarn me.zhanghai.android.materialprogressbar.**
+
+-dontwarn com.sothree.slidinguppanel.**
+
+# Mobius
+-dontwarn org.slf4j.impl.StaticLoggerBinder
+-dontwarn org.slf4j.impl.StaticMDCBinder
+-dontwarn org.slf4j.impl.StaticMarkerBinder
+
+# Kotlin
+-dontwarn kotlinx.atomicfu.AtomicBoolean
+
+-keepattributes Exceptions, Signature, InnerClasses, LineNumberTable, SourceFile, EnclosingMethod
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/CoursesScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/CoursesScreenTest.kt
new file mode 100644
index 0000000000..07772ed7b0
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/CoursesScreenTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.ui.compose
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.espresso.assertTextColor
+import com.instructure.pandares.R
+import com.instructure.parentapp.features.courses.list.CourseListItemUiState
+import com.instructure.parentapp.features.courses.list.CoursesScreen
+import com.instructure.parentapp.features.courses.list.CoursesUiState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@RunWith(AndroidJUnit4::class)
+class CoursesScreenTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertEmptyContent() {
+ composeTestRule.setContent {
+ CoursesScreen(
+ uiState = CoursesUiState(
+ isLoading = false,
+ courseListItems = emptyList()
+ ),
+ actionHandler = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("No Courses")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Your student’s courses might not be published yet.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithTag(R.drawable.ic_panda_book.toString())
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun assertErrorContent() {
+ composeTestRule.setContent {
+ CoursesScreen(
+ uiState = CoursesUiState(
+ isLoading = false,
+ isError = true
+ ),
+ actionHandler = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("There was an error loading your student’s courses.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Retry")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+
+ @Test
+ fun assertCourseContent() {
+ composeTestRule.setContent {
+ CoursesScreen(
+ uiState = CoursesUiState(
+ isLoading = false,
+ studentColor = android.graphics.Color.RED,
+ courseListItems = listOf(
+ CourseListItemUiState(
+ courseId = 1,
+ courseName = "Course 1",
+ courseCode = "C1",
+ grade = "A+"
+ )
+ )
+ ),
+ actionHandler = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("Course 1")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("C1")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("A+", useUnmergedTree = true)
+ .assertIsDisplayed()
+ .assertTextColor(Color(android.graphics.Color.RED))
+ composeTestRule.onNodeWithTag("courseListItem")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+
+ @Test
+ fun assertLoadingContent() {
+ composeTestRule.setContent {
+ CoursesScreen(
+ uiState = CoursesUiState(
+ isLoading = true
+ ),
+ actionHandler = {}
+ )
+ }
+
+ composeTestRule.onNodeWithTag("pullRefreshIndicator")
+ .assertIsDisplayed()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt
new file mode 100644
index 0000000000..bb70b4e921
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.ui.compose
+
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnyChild
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.pandares.R
+import com.instructure.pandautils.utils.ThemedColor
+import com.instructure.parentapp.features.managestudents.ColorPickerDialogUiState
+import com.instructure.parentapp.features.managestudents.ManageStudentsScreen
+import com.instructure.parentapp.features.managestudents.ManageStudentsUiState
+import com.instructure.parentapp.features.managestudents.StudentItemUiState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@RunWith(AndroidJUnit4::class)
+class ManageStudentsScreenTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertEmptyContent() {
+ composeTestRule.setContent {
+ ManageStudentsScreen(
+ uiState = ManageStudentsUiState(
+ isLoading = false,
+ studentListItems = emptyList()
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("You are not observing any students.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Retry")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithTag(R.drawable.panda_manage_students.toString())
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun assertErrorContent() {
+ composeTestRule.setContent {
+ ManageStudentsScreen(
+ uiState = ManageStudentsUiState(
+ isLoadError = true,
+ studentListItems = emptyList()
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("There was an error loading your students.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Retry")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+
+ @Test
+ fun assertStudentListContent() {
+ composeTestRule.setContent {
+ ManageStudentsScreen(
+ uiState = ManageStudentsUiState(
+ studentListItems = listOf(
+ StudentItemUiState(
+ studentId = 1,
+ studentName = "John Doe",
+ studentPronouns = "He/Him",
+ studentColor = ThemedColor(R.color.studentGreen)
+ ),
+ StudentItemUiState(
+ studentId = 2,
+ studentName = "Jane Doe",
+ studentColor = ThemedColor(R.color.studentPink)
+ )
+ )
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+ }
+
+ fun studentItemMatcher(name: String) = hasTestTag("studentListItem") and hasAnyChild(hasText(name))
+ composeTestRule.onNode(studentItemMatcher("John Doe (He/Him)"), true)
+ .assertIsDisplayed()
+ composeTestRule.onNode(studentItemMatcher("Jane Doe"), true)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun assertColorPickerDialogError() {
+ composeTestRule.setContent {
+ ManageStudentsScreen(
+ uiState = ManageStudentsUiState(
+ studentListItems = listOf(
+ StudentItemUiState(
+ studentId = 1,
+ studentName = "John Doe",
+ studentColor = ThemedColor(R.color.studentGreen)
+ )
+ ),
+ colorPickerDialogUiState = ColorPickerDialogUiState(
+ showColorPickerDialog = true,
+ studentId = 1,
+ initialUserColor = null,
+ userColors = emptyList(),
+ isSavingColorError = true
+ )
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("Select Student Color")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("An error occurred while saving your selection. Please try again.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Cancel")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithText("OK")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt
new file mode 100644
index 0000000000..70de846d41
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.ui.compose
+
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.parentapp.features.notaparent.NotAParentScreen
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@RunWith(AndroidJUnit4::class)
+class NotAParentScreenTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertContent() {
+ composeTestRule.setContent {
+ NotAParentScreen(
+ returnToLoginClick = {},
+ onStudentClick = {},
+ onTeacherClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("Not a parent?").assertIsDisplayed()
+ composeTestRule.onNodeWithText("We couldn't find any students associated with your account").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Return to login")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithText("Are you a student or teacher?")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithText("STUDENT")
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun assertAppOptions() {
+ composeTestRule.setContent {
+ NotAParentScreen(
+ returnToLoginClick = {},
+ onStudentClick = {},
+ onTeacherClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("Are you a student or teacher?").performClick()
+ composeTestRule.onNodeWithText("STUDENT")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithText("TEACHER")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt
new file mode 100644
index 0000000000..252a86d226
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.compose.alerts
+
+import android.graphics.Color
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.parentapp.features.alerts.list.AlertsItemUiState
+import com.instructure.parentapp.features.alerts.list.AlertsListItem
+import com.instructure.parentapp.utils.hasDrawable
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Date
+import com.instructure.parentapp.R
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+@RunWith(AndroidJUnit4::class)
+class AlertsListItemTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertAssignmentMissing() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Assignment Missing title",
+ alertType = AlertType.ASSIGNMENT_MISSING,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.waitForIdle()
+
+ composeTestRule.onNodeWithText("Assignment missing").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Assignment Missing title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertAssignmentGradeHigh() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Assignment Grade High title",
+ alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
+ date = Date(),
+ observerAlertThreshold = "90%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Assignment Grade Above 90%").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Assignment Grade High title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertAssignmentGradeLow() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Assignment Grade Low title",
+ alertType = AlertType.ASSIGNMENT_GRADE_LOW,
+ date = Date(),
+ observerAlertThreshold = "60%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Assignment Grade Below 60%").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Assignment Grade Low title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertCourseGradeHigh() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Course Grade High title",
+ alertType = AlertType.COURSE_GRADE_HIGH,
+ date = Date(),
+ observerAlertThreshold = "90%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Course Grade Above 90%").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Course Grade High title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertCourseGradeLow() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Course Grade Low title",
+ alertType = AlertType.COURSE_GRADE_LOW,
+ date = Date(),
+ observerAlertThreshold = "60%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Course Grade Below 60%").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Course Grade Low title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertCourseAnnouncement() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Course Announcement title",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Course Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Course Announcement title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertInstitutionAnnouncement() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Institution Announcement title",
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Institution Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Institution Announcement title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertLockedForUser() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Locked for User title",
+ alertType = AlertType.ASSIGNMENT_MISSING,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = true,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Assignment missing").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Locked for User title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_lock_lined)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertRead() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Institution Announcement title",
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Institution Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Institution Announcement title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ composeTestRule.onNodeWithTag("unreadIndicator").assertIsNotDisplayed()
+ }
+
+ private fun parseDate(date: Date): String {
+ val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
+ val timeFormat = SimpleDateFormat("h:mm a", Locale.getDefault())
+ return "${dateFormat.format(date)} at ${timeFormat.format(date)}"
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt
new file mode 100644
index 0000000000..3f2586ed59
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.compose.alerts
+
+import android.graphics.Color
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnyDescendant
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.parentapp.features.alerts.list.AlertsItemUiState
+import com.instructure.parentapp.features.alerts.list.AlertsScreen
+import com.instructure.parentapp.features.alerts.list.AlertsUiState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.text.SimpleDateFormat
+import java.time.Instant
+import java.util.Date
+import java.util.Locale
+
+@ExperimentalMaterialApi
+@RunWith(AndroidJUnit4::class)
+class AlertsScreenTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertError() {
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = emptyList(),
+ isLoading = false,
+ isError = true,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+
+ composeTestRule.onNodeWithText("There was an error loading your student’s alerts.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Retry").assertHasClickAction().assertIsDisplayed()
+ }
+
+ @Test
+ fun assertLoading() {
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = emptyList(),
+ isLoading = true,
+ isError = false,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithTag("loading").assertIsDisplayed()
+ }
+
+ @Test
+ fun assertEmpty() {
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = emptyList(),
+ isLoading = false,
+ isError = false,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("No Alerts").assertIsDisplayed()
+ composeTestRule.onNodeWithText("There's nothing to be notified of yet.").assertIsDisplayed()
+ }
+
+ @Test
+ fun assertRefreshing() {
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = emptyList(),
+ isLoading = false,
+ isError = false,
+ isRefreshing = true,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithTag("pullRefreshIndicator").assertIsDisplayed()
+ }
+
+ @Test
+ fun assertContent() {
+ val items = listOf(
+ AlertsItemUiState(
+ alertId = 1,
+ title = "Alert 1",
+ alertType = AlertType.ASSIGNMENT_GRADE_LOW,
+ date = Date.from(Instant.parse("2023-09-15T09:02:00Z")),
+ observerAlertThreshold = "20%",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 2,
+ title = "Alert 2",
+ alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
+ date = Date.from(Instant.parse("2023-09-16T09:02:00Z")),
+ observerAlertThreshold = "80%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 3,
+ title = "Alert 3",
+ alertType = AlertType.COURSE_GRADE_LOW,
+ date = Date.from(Instant.parse("2023-09-17T09:02:00Z")),
+ observerAlertThreshold = "20%",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 4,
+ title = "Alert 4",
+ alertType = AlertType.COURSE_GRADE_HIGH,
+ date = Date.from(Instant.parse("2023-09-18T09:02:00Z")),
+ observerAlertThreshold = "50%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 5,
+ title = "Alert 5",
+ alertType = AlertType.ASSIGNMENT_MISSING,
+ date = Date.from(Instant.parse("2023-09-19T09:02:00Z")),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 6,
+ title = "Alert 6",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date.from(Instant.parse("2023-09-20T09:02:00Z")),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 7,
+ title = "Alert 7",
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ date = Date.from(Instant.parse("2023-09-21T09:02:00Z")),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ )
+ )
+
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = items,
+ isLoading = false,
+ isError = false,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+ items.forEach {
+ composeTestRule.onNodeWithText(it.title).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseAlertType(it.alertType, it.observerAlertThreshold)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(it.date!!)).assertIsDisplayed()
+ composeTestRule.onNode(hasAnyDescendant(hasText(it.title)).and(hasTestTag("alertItem")), useUnmergedTree = true).assertHasClickAction()
+ }
+ }
+
+ private fun parseDate(date: Date): String {
+ val dateFormat = SimpleDateFormat("MMM d", Locale.getDefault())
+ val timeFormat = SimpleDateFormat("h:mm a", Locale.getDefault())
+ return "${dateFormat.format(date)} at ${timeFormat.format(date)}"
+ }
+
+ private fun parseAlertType(alertType: AlertType, threshold: String?): String {
+ return when(alertType) {
+ AlertType.ASSIGNMENT_GRADE_LOW -> "Assignment Grade Below $threshold"
+ AlertType.ASSIGNMENT_GRADE_HIGH -> "Assignment Grade Above $threshold"
+ AlertType.COURSE_GRADE_LOW -> "Course Grade Below $threshold"
+ AlertType.COURSE_GRADE_HIGH -> "Course Grade Above $threshold"
+ AlertType.ASSIGNMENT_MISSING -> "Assignment missing"
+ AlertType.COURSE_ANNOUNCEMENT -> "Course Announcement"
+ AlertType.INSTITUTION_ANNOUNCEMENT -> "Institution Announcement"
+ else -> "Unknown"
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/ParentHiltTestApplication.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/ParentHiltTestApplication.kt
new file mode 100644
index 0000000000..133f99ea69
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/ParentHiltTestApplication.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.ui.espresso
+
+import dagger.hilt.android.testing.CustomTestApplication
+
+@CustomTestApplication(TestAppManager::class)
+interface ParentHiltTestApplication
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/db/Db.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/ParentHiltTestRunner.kt
similarity index 51%
rename from apps/student/src/main/java/com/instructure/student/db/Db.kt
rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/ParentHiltTestRunner.kt
index 6881833765..f1d1d18169 100644
--- a/apps/student/src/main/java/com/instructure/student/db/Db.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/ParentHiltTestRunner.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 - present Instructure, Inc.
+ * Copyright (C) 2024 - present Instructure, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,29 +14,16 @@
* limitations under the License.
*
*/
-package com.instructure.student.db
-import com.squareup.sqldelight.db.SqlDriver
+package com.instructure.parentapp.ui.espresso
-object Db {
- private var driverRef: SqlDriver? = null
- private var dbRef: StudentDb? = null
+import android.app.Application
+import android.content.Context
+import com.instructure.canvas.espresso.CanvasRunner
- val ready: Boolean
- get() = driverRef != null
+class ParentHiltTestRunner : CanvasRunner() {
- fun dbSetup(driver: SqlDriver) {
- val db = createQueryWrapper(driver)
- driverRef = driver
- dbRef = db
+ override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
+ return super.newApplication(cl, ParentHiltTestApplication_Application::class.java.name, context)
}
-
- internal fun dbClear() {
- driverRef!!.close()
- dbRef = null
- driverRef = null
- }
-
- val instance: StudentDb
- get() = dbRef!!
-}
\ No newline at end of file
+}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/MigrationUtils.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt
similarity index 58%
rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/MigrationUtils.kt
rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt
index 73b60cf4e5..76c05c44d5 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/MigrationUtils.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 - present Instructure, Inc.
+ * Copyright (C) 2024 - present Instructure, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,15 +15,16 @@
*
*/
-package com.instructure.pandautils.room
+package com.instructure.parentapp.ui.espresso
-import androidx.room.migration.Migration
-import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.work.WorkerFactory
+import com.instructure.parentapp.util.BaseAppManager
-fun createMigration(from: Int, to: Int, migrationBlock: (SupportSQLiteDatabase) -> Unit): Migration {
- return object : Migration(from, to) {
- override fun migrate(database: SupportSQLiteDatabase) {
- migrationBlock(database)
- }
+open class TestAppManager : BaseAppManager() {
+
+ var workerFactory: WorkerFactory? = null
+
+ override fun getWorkManagerFactory(): WorkerFactory {
+ return workerFactory ?: WorkerFactory.getDefaultWorkerFactory()
}
}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt
new file mode 100644
index 0000000000..7a2d099067
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */ package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addObserverAlert
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.canvasapi2.models.AlertWorkflowState
+import com.instructure.parentapp.utils.ParentComposeTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Test
+import java.util.Date
+
+@HiltAndroidTest
+class AlertsInteractionTest : ParentComposeTest() {
+
+ @Test
+ fun dismissAlert() {
+ val data = initData()
+ goToAlerts(data)
+
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val course = data.courses.values.first()
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ course,
+ AlertType.ASSIGNMENT_MISSING,
+ AlertWorkflowState.UNREAD,
+ Date(),
+ null,
+ false
+ )
+
+ alertsPage.refresh()
+
+ alertsPage.assertAlertItemDisplayed(alert.title)
+
+ composeTestRule.waitForIdle()
+ alertsPage.dismissAlert(alert.title)
+
+ composeTestRule.waitForIdle()
+ alertsPage.assertSnackbar("Alert dismissed")
+ alertsPage.assertAlertItemNotDisplayed(alert.title)
+ alertsPage.refresh()
+ alertsPage.assertAlertItemNotDisplayed(alert.title)
+ }
+
+ @Test
+ fun undoDismiss() {
+ val data = initData()
+ goToAlerts(data)
+
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val course = data.courses.values.first()
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ course,
+ AlertType.ASSIGNMENT_MISSING,
+ AlertWorkflowState.UNREAD,
+ Date(),
+ null,
+ false
+ )
+
+ alertsPage.refresh()
+
+ alertsPage.assertAlertItemDisplayed(alert.title)
+
+ composeTestRule.waitForIdle()
+ alertsPage.dismissAlert(alert.title)
+
+ composeTestRule.waitForIdle()
+ alertsPage.assertSnackbar("Alert dismissed")
+ alertsPage.clickUndo()
+ alertsPage.assertAlertItemDisplayed(alert.title)
+
+ alertsPage.refresh()
+ alertsPage.assertAlertItemDisplayed(alert.title)
+ }
+
+ @Test
+ fun emptyAlerts() {
+ val data = initData()
+ goToAlerts(data)
+
+ alertsPage.assertEmptyState()
+ }
+
+ @Test
+ fun openAlert() {
+ val data = initData()
+ goToAlerts(data)
+
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val course = data.courses.values.first()
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ course,
+ AlertType.ASSIGNMENT_MISSING,
+ AlertWorkflowState.UNREAD,
+ Date(),
+ null,
+ false
+ )
+
+ alertsPage.refresh()
+
+ composeTestRule.waitForIdle()
+ alertsPage.assertAlertItemDisplayed(alert.title)
+ alertsPage.assertAlertUnread(alert.title)
+ alertsPage.clickOnAlert(alert.title)
+
+ //TODO check that we route to the correct screen when ready
+ }
+
+ private fun initData(): MockCanvas {
+ return MockCanvas.init(studentCount = 1, parentCount = 1, courseCount = 1)
+ }
+
+ private fun goToAlerts(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickAlerts()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt
new file mode 100644
index 0000000000..b03bded687
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.ui.interaction
+
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment
+import com.instructure.canvas.espresso.mockCanvas.addEnrollment
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.parentapp.ui.pages.CoursesPage
+import com.instructure.parentapp.utils.ParentComposeTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Test
+
+
+@HiltAndroidTest
+class CoursesInteractionTest : ParentComposeTest() {
+
+ private val coursesPage = CoursesPage(composeTestRule)
+
+ @Test
+ fun testNoCourseDisplayed() {
+ val data = initData()
+ data.courses.clear()
+
+ goToCourses(data)
+
+ composeTestRule.waitForIdle()
+ coursesPage.assertEmptyContentDisplayed()
+ }
+
+ @Test
+ fun testCourseDisplayed() {
+ val data = initData()
+
+ goToCourses(data)
+
+ composeTestRule.waitForIdle()
+ coursesPage.assertCourseItemDisplayed(data.courses.values.first())
+ }
+
+ @Test
+ fun testShowGradeIfThereIsACurrentGrade() {
+ val data = initData()
+ val course = data.courses.values.find {
+ val enrollment = it.enrollments!!.first()
+ !enrollment.currentGrade.isNullOrEmpty() && enrollment.currentScore != null
+ }
+ val enrollment = course!!.enrollments!!.first()
+
+ goToCourses(data)
+
+ composeTestRule.waitForIdle()
+ coursesPage.assertGradeTextDisplayed(course.name, "${enrollment.currentGrade} ${enrollment.currentScore}%")
+ }
+
+ @Test
+ fun testShowNoGradeIfThereIsNoCurrentGrade() {
+ val data = initData()
+ val firstStudent = data.students.first()
+ val courseWithoutGrade = data.addCourseWithEnrollment(firstStudent, Enrollment.EnrollmentType.Student, score = null, grade = null)
+ data.addEnrollment(data.parents.first(), courseWithoutGrade, Enrollment.EnrollmentType.Observer, firstStudent)
+
+ goToCourses(data)
+
+ composeTestRule.waitForIdle()
+ coursesPage.assertGradeTextDisplayed(courseWithoutGrade.name, "No Grade")
+ }
+
+ @Test
+ fun testShowGradeOnlyIfQuantitativeDataIsRestricted() {
+ val data = initData()
+ val firstStudent = data.students.first()
+ val course = data.addCourseWithEnrollment(firstStudent, Enrollment.EnrollmentType.Student, restrictQuantitativeData = true)
+ data.addEnrollment(data.parents.first(), course, Enrollment.EnrollmentType.Observer, firstStudent)
+
+ goToCourses(data)
+
+ composeTestRule.waitForIdle()
+ coursesPage.assertGradeTextIsNotDisplayed(course.name)
+ }
+
+ @Test
+ fun testCourseTapped() {
+ val data = initData()
+
+ goToCourses(data)
+
+ composeTestRule.waitForIdle()
+ coursesPage.tapCurseItem(data.courses.values.first().name)
+ // TODO Assert course details when implemented
+ }
+
+ private fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ courseCount = 3
+ )
+ }
+
+ private fun goToCourses(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt
new file mode 100644
index 0000000000..264ce4131e
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvas.espresso.waitForMatcherWithSleeps
+import com.instructure.loginapi.login.R
+import com.instructure.parentapp.utils.ParentTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Test
+
+
+@HiltAndroidTest
+class DashboardInteractionTest : ParentTest() {
+
+ @Test
+ fun testObserverData() {
+ val data = initData()
+
+ goToDashboard(data)
+
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.assertObserverData(data.parents.first())
+ }
+
+ @Test
+ fun testChangeStudent() {
+ val data = initData()
+
+ goToDashboard(data)
+
+ val students = data.students.sortedBy { it.sortableName }
+ dashboardPage.assertSelectedStudent(students.first().shortName!!)
+ dashboardPage.openStudentSelector()
+ dashboardPage.selectStudent(data.students.last().shortName!!)
+ dashboardPage.assertSelectedStudent(students.last().shortName!!)
+ }
+
+ @Test
+ fun testLogout() {
+ val data = initData()
+
+ goToDashboard(data)
+
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.tapLogout()
+ dashboardPage.assertLogoutDialog()
+ dashboardPage.tapOk()
+ waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(
+ ViewAssertions.matches(
+ ViewMatchers.isDisplayed()
+ )
+ )
+ }
+
+ @Test
+ fun testSwitchUsers() {
+ val data = initData()
+
+ goToDashboard(data)
+
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.tapSwitchUsers()
+ waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(
+ ViewAssertions.matches(
+ ViewMatchers.isDisplayed()
+ )
+ )
+ }
+
+ private fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 2,
+ courseCount = 1
+ )
+ }
+
+ private fun goToDashboard(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+ }
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt
new file mode 100644
index 0000000000..9e5b444aec
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.parentapp.ui.pages.ManageStudentsPage
+import com.instructure.parentapp.utils.ParentComposeTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Test
+
+
+@HiltAndroidTest
+class ManageStudentsInteractionTest : ParentComposeTest() {
+
+ private val manageStudentsPage = ManageStudentsPage(composeTestRule)
+
+ @Test
+ fun testStudentsDisplayed() {
+ val data = initData()
+
+ goToManageStudents(data)
+
+ composeTestRule.waitForIdle()
+ data.students.forEach {
+ manageStudentsPage.assertStudentItemDisplayed(it)
+ }
+ }
+
+ @Test
+ fun testStudentTapped() {
+ val data = initData()
+
+ goToManageStudents(data)
+
+ composeTestRule.waitForIdle()
+ manageStudentsPage.tapStudent(data.students.first().shortName!!)
+ // TODO Assert alert settings when implemented
+ }
+
+ @Test
+ fun testColorPickerDialog() {
+ val data = initData()
+
+ goToManageStudents(data)
+
+ composeTestRule.waitForIdle()
+ manageStudentsPage.tapStudentColor(data.students.first().shortName!!)
+ manageStudentsPage.assertColorPickerDialogDisplayed()
+ }
+
+ private fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 3,
+ courseCount = 1
+ )
+ }
+
+ private fun goToManageStudents(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.tapManageStudents()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt
new file mode 100644
index 0000000000..0bccedad38
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.ui.interaction
+
+import android.content.Intent
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCourse
+import com.instructure.canvas.espresso.mockCanvas.addEnrollment
+import com.instructure.canvas.espresso.mockCanvas.addUser
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvas.espresso.mockCanvas.updateUserEnrollments
+import com.instructure.canvas.espresso.waitForMatcherWithSleeps
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.loginapi.login.R
+import com.instructure.parentapp.ui.pages.NotAParentPage
+import com.instructure.parentapp.utils.ParentComposeTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.CoreMatchers
+import org.junit.Test
+
+
+@HiltAndroidTest
+class NotAParentInteractionsTest : ParentComposeTest() {
+
+ private val notAParentPage = NotAParentPage(composeTestRule)
+
+ @Test
+ fun testLogout() {
+ val data = initData()
+ goToNotAParentScreen(data)
+
+ notAParentPage.tapReturnToLogin()
+ waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(
+ ViewAssertions.matches(
+ ViewMatchers.isDisplayed()
+ )
+ )
+ }
+
+ @Test
+ fun testTapStudent() {
+ val data = initData()
+ goToNotAParentScreen(data)
+
+ notAParentPage.expandAppOptions()
+ Intents.init()
+ try {
+ val expectedIntent = CoreMatchers.allOf(
+ IntentMatchers.hasAction(Intent.ACTION_VIEW),
+ CoreMatchers.anyOf(
+ // Could be either of these, depending on whether the play store app is installed
+ IntentMatchers.hasData("market://details?id=com.instructure.candroid"),
+ IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.candroid")
+ )
+ )
+ notAParentPage.tapApp("STUDENT")
+ Intents.intended(expectedIntent)
+ } finally {
+ Intents.release()
+ }
+ }
+
+ @Test
+ fun testTapTeacher() {
+ val data = initData()
+ goToNotAParentScreen(data)
+
+ notAParentPage.expandAppOptions()
+ Intents.init()
+ try {
+ val expectedIntent = CoreMatchers.allOf(
+ IntentMatchers.hasAction(Intent.ACTION_VIEW),
+ CoreMatchers.anyOf(
+ // Could be either of these, depending on whether the play store app is installed
+ IntentMatchers.hasData("market://details?id=com.instructure.teacher"),
+ IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.teacher")
+ )
+ )
+ notAParentPage.tapApp("TEACHER")
+ Intents.intended(expectedIntent)
+ } finally {
+ Intents.release()
+ }
+ }
+
+ private fun initData(): MockCanvas {
+ val data = MockCanvas.init()
+ val parent = data.addUser()
+ val course = data.addCourse()
+ data.addEnrollment(parent, course, Enrollment.EnrollmentType.Observer)
+ data.updateUserEnrollments()
+ return data
+ }
+
+ private fun goToNotAParentScreen(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent, false)
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt
new file mode 100644
index 0000000000..c1db9f2719
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.CalendarInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.User
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentCalendarInteractionTest : CalendarInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToCalendar(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ composeTestRule.waitForIdle()
+ }
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 1,
+ courseCount = 2,
+ favoriteCourseCount = 1
+ )
+ }
+
+ override fun getLoggedInUser(): User {
+ return MockCanvas.data.parents[0]
+ }
+
+ override fun assertAssignmentDetailsTitle(title: String) {
+ // TODO No assertion here, this should be implemented when the Assignment Details page is created
+ }
+
+ override fun assertDiscussionDetailsTitle(title: String) {
+ // TODO No assertion here, this should be implemented when the Assignment Details page is created
+ }
+
+ override fun clickTodayButton() {
+ dashboardPage.clickTodayButton()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt
new file mode 100644
index 0000000000..09365aedb6
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.CreateUpdateEventInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.User
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentCreateUpdateEventInteractionTest : CreateUpdateEventInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToCreateEvent(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnAddButton()
+ calendarScreenPage.clickAddEvent()
+ }
+
+ override fun goToEditEvent(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ val event = data.userCalendarEvents[parent.id]!!.first()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnItem(event.title!!)
+ calendarEventDetailsPage.clickOverflowMenu()
+ calendarEventDetailsPage.clickEditMenu()
+ }
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 0,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+
+ override fun getLoggedInUser(): User = MockCanvas.data.parents[0]
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt
new file mode 100644
index 0000000000..a40c6cc6fd
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.CreateUpdateToDoInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.User
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentCreateUpdateToDoInteractionTest : CreateUpdateToDoInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToCreateToDo(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnAddButton()
+ calendarScreenPage.clickAddTodo()
+ }
+
+ override fun goToEditToDo(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ val todo = data.todos.first()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnItem(todo.plannable.title)
+ calendarToDoDetailsPage.clickToolbarMenu()
+ calendarToDoDetailsPage.clickEditMenu()
+ }
+
+ override fun initData(): MockCanvas {
+ val data = MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 0,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+ data.currentUser = data.parents.first()
+ return data
+ }
+
+ override fun getLoggedInUser(): User = MockCanvas.data.parents[0]
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt
new file mode 100644
index 0000000000..e685a3520c
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.EventDetailsInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentEventDetailsInteractionTest : EventDetailsInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToEventDetails(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ val event = data.courseCalendarEvents.values.first().first()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnItem(event.title!!)
+ }
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 1,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt
new file mode 100644
index 0000000000..b1d5a9aa9c
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.InboxListInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.CanvasContextPermission
+import com.instructure.canvasapi2.models.User
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.util.ParentPrefs
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Before
+
+@HiltAndroidTest
+class ParentInboxListInteractionTest : InboxListInteractionTest() {
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToInbox(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.assertPageObjects()
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.clickInbox()
+ }
+
+ override fun createInitialData(courseCount: Int): MockCanvas {
+ val data = MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ courseCount = courseCount,
+ teacherCount = 1,
+ favoriteCourseCount = courseCount
+ )
+
+ val course1 = data.courses.values.first()
+
+ data.addCoursePermissions(
+ course1.id,
+ CanvasContextPermission(send_messages_all = true, send_messages = true)
+ )
+
+ data.addRecipientsToCourse(
+ course = course1,
+ students = data.students,
+ teachers = data.teachers
+ )
+
+ return data
+ }
+
+ override fun getLoggedInUser(): User {
+ return MockCanvas.data.parents.first()
+ }
+
+ override fun getOtherUser(): User {
+ return MockCanvas.data.teachers.first()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt
new file mode 100644
index 0000000000..e83142be59
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.SettingsInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentSettingsInteractionTest : SettingsInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 3,
+ courseCount = 1
+ )
+ }
+
+ override fun goToSettings(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.tapSettings()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt
new file mode 100644
index 0000000000..2a374c8ad9
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.instructure.parentapp.ui.interaction
+
+import android.app.Activity
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.ToDoDetailsInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.User
+import com.instructure.espresso.InstructureActivityTestRule
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentToDoDetailsInteractionTest : ToDoDetailsInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule: InstructureActivityTestRule =
+ ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun displaysPageObjects() = Unit
+
+ override fun goToToDoDetails(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ val todo = data.todos.first()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnItem(todo.plannable.title)
+ }
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 1,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+ }
+
+ override fun getLoggedInUser(): User = MockCanvas.data.parents.first()
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt
new file mode 100644
index 0000000000..09f206e617
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */ package com.instructure.parentapp.ui.pages
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasAnyAncestor
+import androidx.compose.ui.test.hasAnyDescendant
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onViewWithText
+
+class AlertsPage(private val composeTestRule: ComposeTestRule) : BasePage() {
+
+ fun assertAlertItemDisplayed(title: String) {
+ composeTestRule.onNodeWithText(title).assertIsDisplayed()
+ }
+
+ fun assertAlertItemNotDisplayed(title: String) {
+ composeTestRule.onNodeWithText(title).assertIsNotDisplayed()
+ }
+
+ fun assertEmptyState() {
+ composeTestRule.onNodeWithTag("emptyAlerts").assertIsDisplayed()
+ }
+
+ fun assertAlertRead(title: String) {
+ composeTestRule.onNode(
+ hasTestTag("unreadIndicator")
+ .and(hasAnyAncestor(hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))))),
+ useUnmergedTree = true
+ ).assertIsNotDisplayed()
+ }
+
+ fun assertAlertUnread(title: String) {
+ composeTestRule.onNode(
+ hasTestTag("unreadIndicator")
+ .and(hasAnyAncestor(hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))))),
+ useUnmergedTree = true
+ ).assertIsDisplayed()
+ }
+
+ fun dismissAlert(title: String) {
+ composeTestRule.onNode(
+ hasAnyAncestor(hasAnyDescendant(hasText(title)).and(hasTestTag("alertItem"))).and(
+ hasTestTag(
+ "dismissButton"
+ )
+ ),
+ useUnmergedTree = true
+ ).performClick()
+ }
+
+ fun clickOnAlert(title: String) {
+ composeTestRule.onNode(
+ hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))),
+ useUnmergedTree = true
+ ).performClick()
+ }
+
+ fun refresh() {
+ composeTestRule.onRoot().performTouchInput { swipeDown() }
+ }
+
+ fun assertSnackbar(message: String) {
+ onViewWithText(message).assertDisplayed()
+ }
+
+ fun clickUndo() {
+ onViewWithText("UNDO").click()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt
new file mode 100644
index 0000000000..83c9844e4f
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.ui.pages
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollTo
+import com.instructure.canvasapi2.models.Course
+import com.instructure.composeTest.hasSiblingWithText
+import com.instructure.pandares.R
+
+
+class CoursesPage(private val composeTestRule: ComposeTestRule) {
+
+ fun assertCourseItemDisplayed(course: Course) {
+ composeTestRule.onNodeWithText(course.name)
+ .performScrollTo()
+ .assertIsDisplayed()
+ course.courseCode?.let {
+ composeTestRule.onNode(hasSiblingWithText(course.name).and(hasText(it)), true)
+ .performScrollTo()
+ .assertIsDisplayed()
+ }
+ }
+
+ fun assertEmptyContentDisplayed() {
+ composeTestRule.onNodeWithText("No Courses")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Your student’s courses might not be published yet.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithTag(R.drawable.ic_panda_book.toString())
+ .assertIsDisplayed()
+ }
+
+ fun assertGradeTextDisplayed(courseName: String, gradeText: String) {
+ composeTestRule.onNode(hasSiblingWithText(courseName).and(hasText(gradeText)), true)
+ .performScrollTo()
+ .assertIsDisplayed()
+ }
+
+ fun assertGradeTextIsNotDisplayed(courseName: String) {
+ composeTestRule.onNode(hasSiblingWithText(courseName).and(hasTestTag("gradeText")), true)
+ .assertIsNotDisplayed()
+ }
+
+ fun tapCurseItem(courseName: String) {
+ composeTestRule.onNodeWithText(courseName)
+ .performScrollTo()
+ .assertIsDisplayed()
+ .performClick()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt
new file mode 100644
index 0000000000..0be5021eaa
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.ui.pages
+
+import com.instructure.canvasapi2.models.User
+import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.getStringFromResource
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.onViewWithId
+import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.withAncestor
+import com.instructure.espresso.page.withText
+import com.instructure.parentapp.R
+import org.hamcrest.Matchers.equalToIgnoringCase
+
+class DashboardPage : BasePage(R.id.drawer_layout) {
+
+ private val toolbar by OnViewWithId(R.id.toolbar)
+ private val bottomNavigationView by OnViewWithId(R.id.bottom_nav)
+ private val alertsItem by OnViewWithId(R.id.alerts)
+ private val calendarItem by OnViewWithId(R.id.calendar)
+
+ fun assertObserverData(user: User) {
+ onViewWithText(user.name).assertDisplayed()
+ onViewWithText(user.email.orEmpty()).assertDisplayed()
+ }
+
+ fun openNavigationDrawer() {
+ onViewWithId(R.id.navigationButtonHolder).click()
+ }
+
+ fun assertSelectedStudent(name: String) {
+ onView(withText(name) + withAncestor(R.id.selected_student_container)).assertDisplayed()
+ }
+
+ fun openStudentSelector() {
+ toolbar.click()
+ }
+
+ fun selectStudent(name: String) {
+ onView(withText(name) + withAncestor(R.id.student_list)).click()
+ }
+
+ fun tapLogout() {
+ onViewWithText(R.string.logout).click()
+ }
+
+ fun assertLogoutDialog() {
+ onViewWithText(R.string.logout_warning).assertDisplayed()
+ onViewWithText(equalToIgnoringCase(getStringFromResource(android.R.string.cancel))).assertDisplayed()
+ onViewWithText(equalToIgnoringCase(getStringFromResource(android.R.string.ok))).assertDisplayed()
+ }
+
+ fun tapOk() {
+ onViewWithText(android.R.string.ok).click()
+ }
+
+ fun tapSwitchUsers() {
+ onViewWithText(R.string.navigationDrawerSwitchUsers).click()
+ }
+
+ fun clickInbox() {
+ onViewWithText(R.string.inbox).click()
+ }
+
+ fun clickAlerts() {
+ alertsItem.click()
+ }
+
+ fun clickCalendar() {
+ calendarItem.click()
+ }
+
+ fun clickTodayButton() {
+ onViewWithId(R.id.todayButtonHolder).click()
+ }
+
+ fun tapManageStudents() {
+ onViewWithText(R.string.screenTitleManageStudents).click()
+ }
+
+ fun tapSettings() {
+ onViewWithText(R.string.settings).click()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt
new file mode 100644
index 0000000000..a1b0eb904a
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.ui.pages
+
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnyChild
+import androidx.compose.ui.test.hasAnySibling
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import com.instructure.canvasapi2.models.User
+
+
+class ManageStudentsPage(private val composeTestRule: ComposeTestRule) {
+
+ fun assertStudentItemDisplayed(user: User) {
+ composeTestRule.onNodeWithText(user.shortName.orEmpty())
+ .assertIsDisplayed()
+ composeTestRule.onNode(hasTestTag("studentListItem") and hasAnyChild(hasText(user.shortName.orEmpty())), true)
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+
+ fun tapStudent(name: String) {
+ composeTestRule.onNodeWithText(name)
+ .assertIsDisplayed()
+ .performClick()
+ }
+
+ fun tapStudentColor(name: String) {
+ composeTestRule.onNode(hasTestTag("studentColor") and hasAnySibling(hasText(name)), true)
+ .assertIsDisplayed()
+ .performClick()
+ }
+
+ fun assertColorPickerDialogDisplayed() {
+ composeTestRule.onNodeWithText("Select Student Color")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Cancel")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("OK")
+ .assertIsDisplayed()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt
new file mode 100644
index 0000000000..72ba16cb88
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.ui.pages
+
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+
+
+class NotAParentPage(private val composeTestRule: ComposeTestRule) {
+
+ fun expandAppOptions() {
+ composeTestRule.onNodeWithText("Are you a student or teacher?").performClick()
+ }
+
+ fun tapReturnToLogin() {
+ composeTestRule.onNodeWithText("Return to login").performClick()
+ }
+
+ fun tapApp(appName: String) {
+ composeTestRule.onNodeWithText(appName).performClick()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt
new file mode 100644
index 0000000000..39aa54cdd1
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.utils
+
+import android.app.Activity
+import android.content.Context
+import com.instructure.espresso.InstructureActivityTestRule
+import com.instructure.loginapi.login.util.LoginPrefs
+import com.instructure.loginapi.login.util.PreviousUsersUtils
+import com.instructure.pandautils.utils.PandaAppResetter
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.parentapp.util.ParentPrefs
+
+
+class ParentActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) {
+
+ override fun performReset(context: Context) {
+ PandaAppResetter.reset(context)
+ ParentPrefs.clearPrefs()
+ PreviousUsersUtils.clear(context)
+ LoginPrefs.clearPrefs()
+
+ // We need to set this true so the theme selector won't stop our tests.
+ ThemePrefs.themeSelectionShown = true
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt
new file mode 100644
index 0000000000..85d6f19e0f
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.utils
+
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.AlertsPage
+import org.junit.Rule
+
+
+abstract class ParentComposeTest : ParentTest() {
+
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule()
+
+ protected val alertsPage = AlertsPage(composeTestRule)
+
+ override fun displaysPageObjects() = Unit
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt
new file mode 100644
index 0000000000..1d1a2b3d2f
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.instructure.parentapp.utils
+
+import com.instructure.canvas.espresso.CanvasTest
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+
+
+abstract class ParentTest : CanvasTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ val dashboardPage = DashboardPage()
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt
new file mode 100644
index 0000000000..99c8e6583b
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.utils
+
+import androidx.annotation.DrawableRes
+import androidx.compose.ui.test.SemanticsMatcher
+import com.instructure.canvas.espresso.CanvasTest
+import com.instructure.canvasapi2.models.User
+import com.instructure.pandautils.utils.DrawableId
+import com.instructure.parentapp.features.login.LoginActivity
+
+
+fun CanvasTest.tokenLogin(domain: String, token: String, user: User, assertDashboard: Boolean = true) {
+ activityRule.runOnUiThread {
+ (originalActivity as LoginActivity).loginWithToken(
+ token,
+ domain,
+ user
+ )
+ }
+
+ if (assertDashboard && this is ParentTest) {
+ dashboardPage.assertPageObjects()
+ }
+}
+
+fun hasDrawable(@DrawableRes id: Int): SemanticsMatcher =
+ SemanticsMatcher.expectValue(DrawableId, id)
diff --git a/apps/parent/src/main/AndroidManifest.xml b/apps/parent/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..14ab33d460
--- /dev/null
+++ b/apps/parent/src/main/AndroidManifest.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt
new file mode 100644
index 0000000000..23e6953d61
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.di
+
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.ObserverApi
+import com.instructure.parentapp.features.alerts.list.AlertsRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class AlertsModule {
+
+ @Provides
+ fun provideAlertsRepository(observerApi: ObserverApi, courseApi: CourseAPI.CoursesInterface): AlertsRepository {
+ return AlertsRepository(observerApi, courseApi)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt
new file mode 100644
index 0000000000..7d0f341cf6
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di
+
+import com.instructure.canvasapi2.utils.Analytics
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.loginapi.login.util.PreviousUsersUtils
+import com.instructure.loginapi.login.util.QRLogin
+import com.instructure.pandautils.utils.LogoutHelper
+import com.instructure.parentapp.util.ParentLogoutHelper
+import com.instructure.parentapp.util.ParentPrefs
+import com.instructure.parentapp.util.navigation.Navigation
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class ApplicationModule {
+
+ @Provides
+ fun provideLogoutHelper(): LogoutHelper {
+ return ParentLogoutHelper()
+ }
+
+ @Provides
+ fun provideQRLogin(): QRLogin {
+ return QRLogin
+ }
+
+ @Provides
+ fun provideAnalytics(): Analytics {
+ return Analytics
+ }
+
+ @Provides
+ fun providePreviousUsersUtils(): PreviousUsersUtils {
+ return PreviousUsersUtils
+ }
+
+ @Provides
+ fun provideParentPrefs(): ParentPrefs {
+ return ParentPrefs
+ }
+
+ @Provides
+ @Singleton
+ fun provideNavigation(apiPrefs: ApiPrefs): Navigation {
+ return Navigation(apiPrefs)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/DatabaseModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/DatabaseModule.kt
new file mode 100644
index 0000000000..1d1e147775
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/DatabaseModule.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di
+
+import android.content.Context
+import androidx.room.Room
+import com.instructure.pandautils.room.appdatabase.AppDatabase
+import com.instructure.pandautils.room.appdatabase.appDatabaseMigrations
+import com.instructure.pandautils.room.calendar.CalendarFilterDatabase
+import com.instructure.pandautils.room.calendar.calendarDatabaseMigrations
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class DatabaseModule {
+
+ @Provides
+ @Singleton
+ fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
+ return Room.databaseBuilder(context, AppDatabase::class.java, "db-canvas-parent")
+ .addMigrations(*appDatabaseMigrations)
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideCalendarDatabase(@ApplicationContext context: Context): CalendarFilterDatabase {
+ return Room.databaseBuilder(context, CalendarFilterDatabase::class.java, "db-calendar-parent")
+ .addMigrations(*calendarDatabaseMigrations)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/DefaultBindingsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/DefaultBindingsModule.kt
new file mode 100644
index 0000000000..e80ad959ed
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/DefaultBindingsModule.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di
+
+import com.instructure.pandautils.features.dashboard.edit.EditDashboardRepository
+import com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter
+import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
+import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository
+import com.instructure.pandautils.features.discussion.router.DiscussionRouter
+import com.instructure.pandautils.features.elementary.grades.GradesRouter
+import com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter
+import com.instructure.pandautils.features.elementary.importantdates.ImportantDatesRouter
+import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter
+import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter
+import com.instructure.pandautils.features.offline.sync.SyncRouter
+import com.instructure.pandautils.features.shareextension.ShareExtensionRouter
+import com.instructure.pandautils.utils.ToolbarSetupBehavior
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+
+@Module
+@InstallIn(SingletonComponent::class)
+class DefaultBindingsModule {
+
+ @Provides
+ fun provideDashboardRouter(): DashboardRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideEditDashboardRouter(): EditDashboardRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideEditDashboardRepository(): EditDashboardRepository {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideDiscussionRouteHelperStudentRepository(): DiscussionRouteHelperRepository {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideDiscussionRouter(): DiscussionRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideGradesRouter(): GradesRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideHomeroomRouter(): HomeroomRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideImportantDatesRouter(): ImportantDatesRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideResourcesRouter(): ResourcesRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideScheduleRouter(): ScheduleRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideToolbarSetup(): ToolbarSetupBehavior {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideShareExtensionRouter(): ShareExtensionRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideSyncRouter(): SyncRouter {
+ throw NotImplementedError()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/FragmentModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/FragmentModule.kt
new file mode 100644
index 0000000000..07bffd2025
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/FragmentModule.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.navigation.WebViewRouter
+import com.instructure.parentapp.util.navigation.ParentWebViewRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+/**
+ * Module for various common Fragment scope dependencies that are used in different Fragments.
+ */
+@Module
+@InstallIn(FragmentComponent::class)
+class FragmentModule {
+
+ @Provides
+ fun provideWebViewRouter(activity: FragmentActivity): WebViewRouter {
+ return ParentWebViewRouter(activity)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/LegalModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/LegalModule.kt
new file mode 100644
index 0000000000..e99ad42ecd
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/LegalModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.di
+
+import android.app.Activity
+import com.instructure.pandautils.features.legal.LegalRouter
+import com.instructure.parentapp.features.legal.ParentLegalRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+
+@Module
+@InstallIn(ActivityComponent::class)
+class LegalModule {
+
+ @Provides
+ fun provideLegalRouter(activity: Activity): LegalRouter {
+ return ParentLegalRouter(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt
new file mode 100644
index 0000000000..8b6ccebd9e
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di
+
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.apis.UserAPI
+import com.instructure.parentapp.features.managestudents.ManageStudentsRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class ManageStudentsModule {
+
+ @Provides
+ fun provideManageStudentsRepository(
+ enrollmentsApi: EnrollmentAPI.EnrollmentInterface,
+ userApi: UserAPI.UsersInterface
+ ): ManageStudentsRepository {
+ return ManageStudentsRepository(enrollmentsApi, userApi)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/SettingsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/SettingsModule.kt
new file mode 100644
index 0000000000..f706dbb0b0
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/SettingsModule.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.di
+
+import com.instructure.pandautils.features.settings.SettingsBehaviour
+import com.instructure.pandautils.features.settings.SettingsRouter
+import com.instructure.parentapp.features.settings.ParentSettingsBehaviour
+import com.instructure.parentapp.features.settings.ParentSettingsRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+class SettingsModule {
+
+ @Provides
+ fun provideSettingsBehaviour(): SettingsBehaviour {
+ return ParentSettingsBehaviour()
+ }
+
+ @Provides
+ fun provideSettingsRouter(): SettingsRouter {
+ return ParentSettingsRouter()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AboutModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AboutModule.kt
new file mode 100644
index 0000000000..c93501e8b1
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AboutModule.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import android.content.Context
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.about.AboutRepository
+import com.instructure.parentapp.features.about.ParentAboutRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class AboutModule {
+
+ @Provides
+ fun provideAboutRepository(
+ @ApplicationContext context: Context,
+ apiPrefs: ApiPrefs
+ ): AboutRepository {
+ return ParentAboutRepository(context, apiPrefs)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CalendarModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CalendarModule.kt
new file mode 100644
index 0000000000..9f8aa5efdc
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CalendarModule.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.di.feature
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.apis.CalendarEventAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.FeaturesAPI
+import com.instructure.canvasapi2.apis.PlannerAPI
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.calendar.CalendarRepository
+import com.instructure.pandautils.features.calendar.CalendarRouter
+import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao
+import com.instructure.parentapp.features.calendar.ParentCalendarRepository
+import com.instructure.parentapp.features.calendar.ParentCalendarRouter
+import com.instructure.parentapp.util.ParentPrefs
+import com.instructure.parentapp.util.navigation.Navigation
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class CalendarModule {
+
+ @Provides
+ fun provideCalendarRouter(activity: FragmentActivity, navigation: Navigation): CalendarRouter {
+ return ParentCalendarRouter(activity, navigation)
+ }
+}
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class CalendarViewModelModule {
+
+ @Provides
+ fun provideCalendarRepository(
+ plannerApi: PlannerAPI.PlannerInterface,
+ coursesApi: CourseAPI.CoursesInterface,
+ calendarEventsApi: CalendarEventAPI.CalendarEventInterface,
+ apiPrefs: ApiPrefs,
+ featuresApi: FeaturesAPI.FeaturesInterface,
+ parentPrefs: ParentPrefs,
+ calendarFilterDao: CalendarFilterDao
+ ): CalendarRepository {
+ return ParentCalendarRepository(plannerApi, coursesApi, calendarEventsApi, apiPrefs, featuresApi, parentPrefs, calendarFilterDao)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CoursesModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CoursesModule.kt
new file mode 100644
index 0000000000..0a6dd85b9a
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CoursesModule.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import android.content.Context
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.parentapp.features.courses.list.CourseGradeFormatter
+import com.instructure.parentapp.features.courses.list.CoursesRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class CoursesModule {
+
+ @Provides
+ fun provideCoursesRepository(courseApi: CourseAPI.CoursesInterface): CoursesRepository {
+ return CoursesRepository(courseApi)
+ }
+
+ @Provides
+ fun provideCourseGradeFormatter(@ApplicationContext context: Context): CourseGradeFormatter {
+ return CourseGradeFormatter(context)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateEventModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateEventModule.kt
new file mode 100644
index 0000000000..9a4e3933a8
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateEventModule.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import com.instructure.canvasapi2.apis.CalendarEventAPI
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventRepository
+import com.instructure.parentapp.features.calendarevent.ParentCreateUpdateEventRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class CreateUpdateEventModule {
+
+ @Provides
+ fun provideCreateUpdateEventRepository(
+ calendarEventApi: CalendarEventAPI.CalendarEventInterface,
+ apiPrefs: ApiPrefs
+ ): CreateUpdateEventRepository {
+ return ParentCreateUpdateEventRepository(calendarEventApi, apiPrefs)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateToDoModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateToDoModule.kt
new file mode 100644
index 0000000000..756137158f
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateToDoModule.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.di.feature
+
+import com.instructure.canvasapi2.apis.CalendarEventAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.PlannerAPI
+import com.instructure.canvasapi2.di.PLANNER_API_SERIALIZE_NULLS
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventRepository
+import com.instructure.pandautils.features.calendartodo.createupdate.CreateUpdateToDoRepository
+import com.instructure.parentapp.features.calendarevent.ParentCreateUpdateEventRepository
+import com.instructure.parentapp.features.calendartodo.ParentCreateUpdateToDoRepository
+import com.instructure.parentapp.util.ParentPrefs
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import javax.inject.Named
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class CreateUpdateToDoModule {
+
+ @Provides
+ fun provideCreateUpdateToDoRepository(
+ coursesApi: CourseAPI.CoursesInterface,
+ parentPrefs: ParentPrefs,
+ @Named(PLANNER_API_SERIALIZE_NULLS) plannerApi: PlannerAPI.PlannerInterface
+ ): CreateUpdateToDoRepository {
+ return ParentCreateUpdateToDoRepository(coursesApi, parentPrefs, plannerApi)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt
new file mode 100644
index 0000000000..4c575ef2a6
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.apis.UnreadCountAPI
+import com.instructure.parentapp.features.dashboard.AlertCountUpdater
+import com.instructure.parentapp.features.dashboard.AlertCountUpdaterImpl
+import com.instructure.parentapp.features.dashboard.DashboardRepository
+import com.instructure.parentapp.features.dashboard.InboxCountUpdater
+import com.instructure.parentapp.features.dashboard.InboxCountUpdaterImpl
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolderImpl
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class DashboardModule {
+
+ @Provides
+ fun provideDashboardRepository(
+ enrollmentApi: EnrollmentAPI.EnrollmentInterface,
+ unreadCountsApi: UnreadCountAPI.UnreadCountsInterface
+ ): DashboardRepository {
+ return DashboardRepository(enrollmentApi, unreadCountsApi)
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class SelectedStudentHolderModule {
+
+ @Provides
+ @Singleton
+ fun provideSelectedStudentHolder(): SelectedStudentHolder {
+ return SelectedStudentHolderImpl()
+ }
+
+ @Provides
+ @Singleton
+ fun provideInboxCountUpdater(): InboxCountUpdater {
+ return InboxCountUpdaterImpl()
+ }
+
+ @Provides
+ @Singleton
+ fun provideAlertCountUpdater(): AlertCountUpdater {
+ return AlertCountUpdaterImpl()
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/EventModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/EventModule.kt
new file mode 100644
index 0000000000..cfcb31ec97
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/EventModule.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.calendarevent.details.EventRouter
+import com.instructure.parentapp.features.calendarevent.ParentEventRouter
+import com.instructure.parentapp.util.navigation.Navigation
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class EventModule {
+
+ @Provides
+ fun provideEventRouter(activity: FragmentActivity, navigation: Navigation): EventRouter {
+ return ParentEventRouter(activity, navigation)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/HelpDialogModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/HelpDialogModule.kt
new file mode 100644
index 0000000000..3467d30c71
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/HelpDialogModule.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.help.HelpDialogFragmentBehavior
+import com.instructure.pandautils.features.help.HelpLinkFilter
+import com.instructure.parentapp.features.help.ParentHelpDialogFragmentBehavior
+import com.instructure.parentapp.features.help.ParentHelpLinkFilter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class HelpDialogModule {
+
+ @Provides
+ fun provideHelpLinkFilter(): HelpLinkFilter {
+ return ParentHelpLinkFilter()
+ }
+}
+
+@Module
+@InstallIn(FragmentComponent::class)
+class HelpDialogFragmentModule {
+
+ @Provides
+ fun provideHelpDialogFragmentBehavior(activity: FragmentActivity): HelpDialogFragmentBehavior {
+ return ParentHelpDialogFragmentBehavior(activity)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt
new file mode 100644
index 0000000000..7b6dc43d5b
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.GroupAPI
+import com.instructure.canvasapi2.apis.InboxApi
+import com.instructure.canvasapi2.apis.ProgressAPI
+import com.instructure.pandautils.features.inbox.list.InboxRepository
+import com.instructure.pandautils.features.inbox.list.InboxRouter
+import com.instructure.parentapp.features.inbox.list.ParentInboxRepository
+import com.instructure.parentapp.features.inbox.list.ParentInboxRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class InboxFragmentModule {
+
+ @Provides
+ fun provideInboxRouter(activity: FragmentActivity, fragment: Fragment): InboxRouter {
+ return ParentInboxRouter(activity, fragment)
+ }
+}
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class InboxModule {
+
+ @Provides
+ fun provideInboxRepository(
+ inboxApi: InboxApi.InboxInterface,
+ coursesApi: CourseAPI.CoursesInterface,
+ groupsApi: GroupAPI.GroupInterface,
+ progressApi: ProgressAPI.ProgressInterface
+ ): InboxRepository {
+ return ParentInboxRepository(inboxApi, coursesApi, groupsApi, progressApi)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt
new file mode 100644
index 0000000000..8880972b59
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.loginapi.login.LoginNavigation
+import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter
+import com.instructure.parentapp.features.login.ParentAcceptableUsePolicyRouter
+import com.instructure.parentapp.features.login.ParentLoginNavigation
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+
+@Module
+@InstallIn(ActivityComponent::class)
+class LoginModule {
+
+ @Provides
+ fun provideAcceptableUsePolicyRouter(activity: FragmentActivity): AcceptableUsePolicyRouter {
+ return ParentAcceptableUsePolicyRouter(activity)
+ }
+
+ @Provides
+ fun provideLoginNavigation(activity: FragmentActivity): LoginNavigation {
+ return ParentLoginNavigation(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt
new file mode 100644
index 0000000000..fa964f67de
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.apis.ThemeAPI
+import com.instructure.canvasapi2.apis.UserAPI
+import com.instructure.parentapp.features.splash.SplashRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class SplashModule {
+
+ @Provides
+ fun provideSplashRepository(
+ userApi: UserAPI.UsersInterface,
+ themeApi: ThemeAPI.ThemeInterface,
+ enrollmentApi: EnrollmentAPI.EnrollmentInterface
+ ): SplashRepository {
+ return SplashRepository(userApi, themeApi, enrollmentApi)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/ToDoModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/ToDoModule.kt
new file mode 100644
index 0000000000..dd699ea531
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/ToDoModule.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.di.feature
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.calendartodo.details.ToDoRouter
+import com.instructure.parentapp.features.calendartodo.ParentToDoRouter
+import com.instructure.parentapp.util.navigation.Navigation
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class ToDoModule {
+
+ @Provides
+ fun provideToDoRouter(activity: FragmentActivity, navigation: Navigation): ToDoRouter {
+ return ParentToDoRouter(activity, navigation)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/about/ParentAboutRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/about/ParentAboutRepository.kt
new file mode 100644
index 0000000000..ca1c822b1d
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/about/ParentAboutRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.features.about
+
+import android.content.Context
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.about.AboutRepository
+import com.instructure.parentapp.BuildConfig
+
+class ParentAboutRepository(context: Context, apiPrefs: ApiPrefs) :
+ AboutRepository(context, apiPrefs) {
+
+ override val appVersion: String = BuildConfig.VERSION_NAME
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt
new file mode 100644
index 0000000000..74a2c9bdf0
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.parentapp.features.alerts.list
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.snackbar.Snackbar
+import com.instructure.pandautils.utils.collectOneOffEvents
+import com.instructure.parentapp.R
+import com.instructure.parentapp.util.navigation.Navigation
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class AlertsFragment : Fragment() {
+
+ @Inject
+ lateinit var navigation: Navigation
+
+ private val viewModel: AlertsViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction)
+ return ComposeView(requireActivity()).apply {
+ setContent {
+ val uiState by viewModel.uiState.collectAsState()
+ AlertsScreen(uiState = uiState, actionHandler = viewModel::handleAction)
+ }
+ }
+ }
+
+ private fun handleAction(action: AlertsViewModelAction) {
+ when (action) {
+ is AlertsViewModelAction.Navigate -> {
+ navigation.navigate(activity, action.route)
+ }
+
+ is AlertsViewModelAction.ShowSnackbar -> {
+ Snackbar.make(requireView(), action.message, Snackbar.LENGTH_SHORT).apply {
+ action.action?.let { setAction(it) { action.actionCallback?.invoke() } }
+ setActionTextColor(resources.getColor(R.color.white, resources.newTheme()))
+ }.show()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt
new file mode 100644
index 0000000000..70b932d0cf
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.features.alerts.list
+
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.ObserverApi
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.Alert
+import com.instructure.canvasapi2.models.AlertThreshold
+import com.instructure.canvasapi2.models.AlertWorkflowState
+import com.instructure.canvasapi2.models.CourseSettings
+import com.instructure.canvasapi2.utils.depaginate
+
+class AlertsRepository(
+ private val observerApi: ObserverApi,
+ private val courseApi: CourseAPI.CoursesInterface
+) {
+
+ suspend fun getAlertsForStudent(studentId: Long, forceNetwork: Boolean): List {
+ val restParams = RestParams(isForceReadFromNetwork = forceNetwork)
+ val allAlerts = observerApi.getObserverAlerts(studentId, restParams).depaginate {
+ observerApi.getNextPageObserverAlerts(it, restParams)
+ }.dataOrThrow.sortedByDescending { it.actionDate }
+
+ val coursesMap = mutableMapOf()
+ val filteredAlerts = allAlerts.filter { alert ->
+ if (!alert.isQuantitativeRestrictionApplies()) return@filter true
+
+ alert.getCourseId()?.let { courseId ->
+ val settings = coursesMap.getOrPut(courseId) {
+ courseApi.getCourseSettings(courseId, restParams).dataOrNull
+ }
+ settings?.restrictQuantitativeData?.not() ?: true
+ } ?: true
+ }
+
+ return filteredAlerts
+ }
+
+ suspend fun getAlertThresholdForStudent(
+ studentId: Long,
+ forceNetwork: Boolean
+ ): List {
+ val restParams = RestParams(isForceReadFromNetwork = forceNetwork)
+ return observerApi.getObserverAlertThresholds(studentId, restParams).dataOrNull
+ ?: emptyList()
+ }
+
+ suspend fun updateAlertWorkflow(alertId: Long, workflowState: AlertWorkflowState): Alert {
+ val restParams = RestParams(isForceReadFromNetwork = true)
+ return observerApi.updateAlertWorkflow(
+ alertId,
+ workflowState.name.lowercase(),
+ restParams
+ ).dataOrThrow
+ }
+
+ suspend fun getUnreadAlertCount(studentId: Long): Int {
+ val alerts = try {
+ getAlertsForStudent(studentId, true)
+ } catch (e: Exception) {
+ emptyList()
+ }
+ return alerts.count { it.workflowState == AlertWorkflowState.UNREAD }
+ }
+
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt
new file mode 100644
index 0000000000..01739d777d
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+@file:OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
+
+package com.instructure.parentapp.features.alerts.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.canvasapi2.utils.ContextKeeper
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.pandautils.R
+import com.instructure.pandautils.compose.CanvasTheme
+import com.instructure.pandautils.compose.composables.EmptyContent
+import com.instructure.pandautils.compose.composables.ErrorContent
+import com.instructure.pandautils.compose.composables.Loading
+import com.instructure.pandautils.utils.drawableId
+import java.util.Date
+
+
+@Composable
+fun AlertsScreen(
+ uiState: AlertsUiState,
+ actionHandler: (AlertsAction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ CanvasTheme {
+ Scaffold(
+ backgroundColor = colorResource(id = R.color.backgroundLightest),
+ content = { padding ->
+ val pullRefreshState = rememberPullRefreshState(
+ refreshing = uiState.isRefreshing,
+ onRefresh = {
+ actionHandler(AlertsAction.Refresh)
+ }
+ )
+ Box(modifier = modifier.pullRefresh(pullRefreshState)) {
+ when {
+ uiState.isError -> {
+ ErrorContent(
+ errorMessage = stringResource(id = R.string.errorLoadingAlerts),
+ retryClick = {
+ actionHandler(AlertsAction.Refresh)
+ }, modifier = Modifier.fillMaxSize()
+ )
+ }
+
+ uiState.isLoading -> {
+ Loading(
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("loading"),
+ color = Color(uiState.studentColor)
+ )
+ }
+
+ uiState.alerts.isEmpty() -> {
+ EmptyContent(
+ emptyTitle = stringResource(id = R.string.parentNoAlerts),
+ emptyMessage = stringResource(id = R.string.parentNoAlersMessage),
+ imageRes = R.drawable.ic_panda_noalerts,
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("emptyAlerts")
+ .verticalScroll(rememberScrollState())
+ )
+ }
+
+ else -> {
+ AlertsListContent(
+ uiState = uiState,
+ actionHandler = actionHandler,
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ )
+ }
+ }
+ PullRefreshIndicator(
+ refreshing = uiState.isRefreshing,
+ state = pullRefreshState,
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .testTag("pullRefreshIndicator"),
+ contentColor = Color(uiState.studentColor)
+ )
+ }
+
+ },
+ modifier = modifier
+ )
+ }
+}
+
+@Composable
+fun AlertsListContent(
+ uiState: AlertsUiState,
+ actionHandler: (AlertsAction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ LazyColumn(
+ modifier = modifier
+ ) {
+ items(uiState.alerts, key = { it.alertId }) { alert ->
+ AlertsListItem(
+ alert = alert,
+ userColor = uiState.studentColor,
+ actionHandler = actionHandler,
+ modifier = Modifier.animateItemPlacement()
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ }
+ }
+}
+
+@Composable
+fun AlertsListItem(
+ alert: AlertsItemUiState,
+ userColor: Int,
+ actionHandler: (AlertsAction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+
+ fun alertTitle(alertType: AlertType, alertThreshold: String?): String {
+ val threshold = alertThreshold.orEmpty()
+ return when (alertType) {
+ AlertType.ASSIGNMENT_MISSING -> context.getString(R.string.assignmentMissingAlertTitle)
+ AlertType.ASSIGNMENT_GRADE_HIGH -> context.getString(
+ R.string.assignmentGradeHighAlertTitle,
+ threshold
+ )
+ AlertType.ASSIGNMENT_GRADE_LOW -> context.getString(
+ R.string.assignmentGradeLowAlertTitle,
+ threshold
+ )
+ AlertType.COURSE_GRADE_HIGH -> context.getString(
+ R.string.courseGradeHighAlertTitle,
+ threshold
+ )
+ AlertType.COURSE_GRADE_LOW -> context.getString(
+ R.string.courseGradeLowAlertTitle,
+ threshold
+ )
+ AlertType.COURSE_ANNOUNCEMENT -> context.getString(R.string.courseAnnouncementAlertTitle)
+ AlertType.INSTITUTION_ANNOUNCEMENT -> context.getString(R.string.institutionAnnouncementAlertTitle)
+ }
+ }
+
+ fun alertIcon(alertType: AlertType, lockedForUser: Boolean): Int {
+ return when {
+ lockedForUser -> R.drawable.ic_lock_lined
+ alertType.isAlertInfo() || alertType.isAlertPositive() -> R.drawable.ic_info
+ alertType.isAlertNegative() -> R.drawable.ic_warning
+ else -> R.drawable.ic_info
+ }
+ }
+
+ fun alertColor(alertType: AlertType): Int {
+ return when {
+ alertType.isAlertInfo() -> context.getColor(R.color.textDark)
+ alertType.isAlertNegative() -> context.getColor(R.color.textDanger)
+ alertType.isAlertPositive() -> userColor
+ else -> context.getColor(R.color.textDark)
+ }
+ }
+
+ fun dateTime(dateTime: Date): String {
+ val date = DateHelper.getDayMonthDateString(context, dateTime)
+ val time = DateHelper.getFormattedTime(context, dateTime)
+
+ return context.getString(R.string.alertDateTime, date, time)
+ }
+
+ Row(modifier = modifier
+ .fillMaxWidth()
+ .clickable(enabled = alert.htmlUrl != null) {
+ alert.htmlUrl?.let {
+ actionHandler(AlertsAction.Navigate(alert.alertId, it))
+ }
+ }
+ .padding(8.dp)
+ .testTag("alertItem"),
+ verticalAlignment = Alignment.CenterVertically) {
+ Row(modifier = Modifier.align(Alignment.Top)) {
+ if (alert.unread) {
+ Box(
+ modifier = Modifier
+ .size(8.dp)
+ .clip(CircleShape)
+ .background(Color(userColor))
+ .testTag("unreadIndicator")
+ )
+ }
+
+ val iconId = alertIcon(alert.alertType, alert.lockedForUser)
+ Icon(
+ modifier = Modifier
+ .padding(start = if (alert.unread) 0.dp else 8.dp, end = 32.dp)
+ .semantics {
+ drawableId = iconId
+ },
+ painter = painterResource(id = iconId),
+ contentDescription = null,
+ tint = Color(alertColor(alert.alertType))
+ )
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = alertTitle(alert.alertType, alert.observerAlertThreshold),
+ style = TextStyle(color = Color(alertColor(alert.alertType)), fontSize = 12.sp)
+ )
+ Text(
+ modifier = Modifier.padding(vertical = 4.dp),
+ text = alert.title,
+ style = TextStyle(color = colorResource(id = R.color.textDarkest), fontSize = 16.sp)
+ )
+ alert.date?.let {
+ Text(
+ text = dateTime(alert.date),
+ style = TextStyle(
+ color = colorResource(id = R.color.textDark),
+ fontSize = 12.sp
+ )
+ )
+ }
+ }
+ IconButton(
+ modifier = Modifier
+ .testTag("dismissButton"),
+ onClick = { actionHandler(AlertsAction.DismissAlert(alert.alertId)) }) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close),
+ tint = colorResource(id = R.color.textDark),
+ contentDescription = stringResource(
+ id = R.string.a11y_contentDescription_observerAlertDelete
+ )
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun AlertsScreenPreview() {
+ AlertsScreen(
+ uiState = AlertsUiState(
+ alerts = listOf(
+ AlertsItemUiState(
+ alertId = 1L,
+ title = "Alert title",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 2L,
+ title = "Assignment missing",
+ alertType = AlertType.ASSIGNMENT_MISSING,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 3L,
+ title = "Course grade low",
+ alertType = AlertType.COURSE_GRADE_LOW,
+ date = Date(),
+ observerAlertThreshold = "8",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 4L,
+ title = "Course grade high",
+ alertType = AlertType.COURSE_GRADE_HIGH,
+ date = Date(),
+ observerAlertThreshold = "80%",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 5L,
+ title = "Institution announcement",
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 6L,
+ title = "Assignment grade low",
+ alertType = AlertType.ASSIGNMENT_GRADE_LOW,
+ date = Date(),
+ observerAlertThreshold = "8",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 7L,
+ title = "Assignment grade high",
+ alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
+ date = Date(),
+ observerAlertThreshold = "80%",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 8L,
+ title = "Locked alert",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = true,
+ unread = false,
+ htmlUrl = ""
+ )
+ )
+ ),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsScreenErrorPreview() {
+ AlertsScreen(
+ uiState = AlertsUiState(isError = true),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsScreenEmptyPreview() {
+ AlertsScreen(
+ uiState = AlertsUiState(),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsScreenLoadingPreview() {
+ ContextKeeper.appContext = LocalContext.current
+ AlertsScreen(
+ uiState = AlertsUiState(isLoading = true),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsScreenRefreshingPreview() {
+ AlertsScreen(
+ uiState = AlertsUiState(isRefreshing = true),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsListItemPreview() {
+ AlertsListItem(
+ alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Alert title",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = ""
+ ),
+ userColor = Color.Blue.toArgb(),
+ actionHandler = {}
+ )
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt
new file mode 100644
index 0000000000..f9199fcd11
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */ package com.instructure.parentapp.features.alerts.list
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import com.instructure.canvasapi2.models.AlertType
+import java.util.Date
+
+data class AlertsUiState(
+ val alerts: List = emptyList(),
+ @ColorInt val studentColor: Int = Color.BLACK,
+ val isLoading: Boolean = false,
+ val isError: Boolean = false,
+ val isRefreshing: Boolean = false
+)
+
+data class AlertsItemUiState(
+ val alertId: Long,
+ val title: String,
+ val alertType: AlertType,
+ val date: Date?,
+ val observerAlertThreshold: String?,
+ val lockedForUser: Boolean,
+ val unread: Boolean,
+ val htmlUrl: String?
+)
+
+sealed class AlertsViewModelAction {
+ data class Navigate(val route: String): AlertsViewModelAction()
+ data class ShowSnackbar(val message: Int, val action: Int?, val actionCallback: (() -> Unit)?): AlertsViewModelAction()
+}
+
+sealed class AlertsAction {
+ data object Refresh : AlertsAction()
+ data class Navigate(val alertId: Long, val route: String) : AlertsAction()
+ data class DismissAlert(val alertId: Long) : AlertsAction()
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt
new file mode 100644
index 0000000000..84ee8ac7a9
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.features.alerts.list
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.models.Alert
+import com.instructure.canvasapi2.models.AlertThreshold
+import com.instructure.canvasapi2.models.AlertWorkflowState
+import com.instructure.canvasapi2.models.User
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.parentapp.R
+import com.instructure.parentapp.features.dashboard.AlertCountUpdater
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AlertsViewModel @Inject constructor(
+ private val repository: AlertsRepository,
+ private val colorKeeper: ColorKeeper,
+ private val selectedStudentHolder: SelectedStudentHolder,
+ private val alertCountUpdater: AlertCountUpdater
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(AlertsUiState())
+ val uiState = _uiState.asStateFlow()
+
+ private val _events = Channel()
+ val events = _events.receiveAsFlow()
+
+ private var selectedStudent: User? = null
+ private var thresholds: Map = emptyMap()
+
+ init {
+ viewModelScope.launch {
+ selectedStudentHolder.selectedStudentState.collectLatest {
+ studentChanged(it)
+ }
+ }
+ }
+
+ private suspend fun studentChanged(student: User?) {
+ if (selectedStudent != student) {
+ selectedStudent = student
+ _uiState.update {
+ it.copy(
+ studentColor = colorKeeper.getOrGenerateUserColor(student).textAndIconColor(),
+ isLoading = true
+ )
+ }
+ loadThresholds()
+ loadAlerts()
+ }
+ }
+
+ private suspend fun loadThresholds(forceNetwork: Boolean = false) {
+ selectedStudent?.let { student ->
+ val thresholds = repository.getAlertThresholdForStudent(student.id, forceNetwork)
+ this.thresholds = thresholds.associateBy { it.id }
+ }
+ }
+
+ private suspend fun loadAlerts(forceNetwork: Boolean = false) {
+ selectedStudent?.let { student ->
+ try {
+ val alerts = repository.getAlertsForStudent(student.id, forceNetwork)
+ val alertItems = alerts.map { createAlertItem(it) }
+ _uiState.update {
+ it.copy(
+ alerts = alertItems,
+ isLoading = false,
+ isError = false,
+ isRefreshing = false,
+ )
+ }
+ } catch (e: Exception) {
+ setError()
+ }
+ } ?: setError()
+
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ }
+
+ private fun setError() {
+ _uiState.update {
+ it.copy(isLoading = false, isError = true, isRefreshing = false, alerts = emptyList())
+ }
+ }
+
+ fun handleAction(action: AlertsAction) {
+ when (action) {
+ is AlertsAction.Navigate -> {
+ viewModelScope.launch {
+ _events.send(AlertsViewModelAction.Navigate(action.route))
+ markAlertRead(action.alertId)
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ }
+ }
+
+ is AlertsAction.Refresh -> {
+ viewModelScope.launch {
+ _uiState.update { it.copy(isRefreshing = true) }
+ loadThresholds(true)
+ loadAlerts(true)
+ }
+ }
+
+ is AlertsAction.DismissAlert -> {
+ viewModelScope.launch {
+ dismissAlert(action.alertId)
+ }
+ }
+ }
+ }
+
+ private suspend fun markAlertRead(alertId: Long) {
+ try {
+ _uiState.update { uiState ->
+ uiState.copy(
+ alerts = uiState.alerts.map { alertItem ->
+ if (alertItem.alertId == alertId) alertItem.copy(unread = false) else alertItem
+ }
+ )
+ }
+ repository.updateAlertWorkflow(alertId, AlertWorkflowState.READ)
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ } catch (e: Exception) {
+ //No need to do anything. The alert will stay read.
+ }
+ }
+
+ private suspend fun dismissAlert(alertId: Long) {
+ fun resetAlert(alert: AlertsItemUiState) {
+ val alerts = _uiState.value.alerts.toMutableList()
+ alerts.add(alert)
+ alerts.sortByDescending { it.date }
+ _uiState.update { it.copy(alerts = alerts) }
+ viewModelScope.launch {
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ }
+ }
+
+ val alerts = _uiState.value.alerts.toMutableList()
+ val alert = alerts.find { it.alertId == alertId } ?: return
+ alerts.removeIf { it.alertId == alertId }
+ _uiState.update { it.copy(alerts = alerts) }
+
+ try {
+ repository.updateAlertWorkflow(alertId, AlertWorkflowState.DISMISSED)
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissMessage, R.string.alertDismissAction) {
+ viewModelScope.launch {
+ try {
+ repository.updateAlertWorkflow(
+ alert.alertId,
+ if (alert.unread) AlertWorkflowState.UNREAD else AlertWorkflowState.READ
+ )
+ resetAlert(alert)
+ } catch (e: Exception) {
+ _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissActionErrorMessage, null, null))
+ }
+ }
+ })
+ } catch (e: Exception) {
+ _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissErrorMessage, null, null))
+ resetAlert(alert)
+ }
+ }
+
+ private fun createAlertItem(alert: Alert): AlertsItemUiState {
+ return AlertsItemUiState(
+ alertId = alert.id,
+ title = alert.title,
+ alertType = alert.alertType,
+ date = alert.actionDate,
+ observerAlertThreshold = thresholds[alert.observerAlertThresholdId]?.threshold,
+ lockedForUser = alert.lockedForUser,
+ unread = alert.workflowState == AlertWorkflowState.UNREAD,
+ htmlUrl = alert.htmlUrl
+ )
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt
new file mode 100644
index 0000000000..9f5d6762b0
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.features.calendar
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.instructure.pandautils.features.calendar.BaseCalendarFragment
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
+import com.instructure.parentapp.util.ParentPrefs
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ParentCalendarFragment : BaseCalendarFragment() {
+
+ @Inject
+ lateinit var selectedStudentHolder: SelectedStudentHolder
+
+ override fun onStart() {
+ super.onStart()
+ lifecycleScope.launch {
+ selectedStudentHolder.selectedStudentChangedFlow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collectLatest {
+ delay(100)
+ refreshCalendar()
+ }
+ }
+ }
+
+ override fun applyTheme() {
+ val student = ParentPrefs.currentStudent
+ val color = ColorKeeper.getOrGenerateUserColor(student).backgroundColor()
+ ViewStyler.setStatusBarDark(requireActivity(), color)
+ }
+
+ override fun showToolbar(): Boolean = false
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt
new file mode 100644
index 0000000000..2a12e6151d
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.parentapp.features.calendar
+
+import com.instructure.canvasapi2.apis.CalendarEventAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.FeaturesAPI
+import com.instructure.canvasapi2.apis.PlannerAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Plannable
+import com.instructure.canvasapi2.models.PlannableType
+import com.instructure.canvasapi2.models.PlannerItem
+import com.instructure.canvasapi2.models.toPlannerItems
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.DataResult
+import com.instructure.canvasapi2.utils.depaginate
+import com.instructure.canvasapi2.utils.toDate
+import com.instructure.pandautils.features.calendar.CalendarRepository
+import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao
+import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity
+import com.instructure.pandautils.utils.orDefault
+import com.instructure.parentapp.util.ParentPrefs
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+
+class ParentCalendarRepository(
+ private val plannerApi: PlannerAPI.PlannerInterface,
+ private val coursesApi: CourseAPI.CoursesInterface,
+ private val calendarEventApi: CalendarEventAPI.CalendarEventInterface,
+ private val apiPrefs: ApiPrefs,
+ private val featuresApi: FeaturesAPI.FeaturesInterface,
+ private val parentPrefs: ParentPrefs,
+ private val calendarFilterDao: CalendarFilterDao
+) : CalendarRepository {
+
+ private var canvasContexts: List = emptyList()
+
+ override suspend fun getPlannerItems(
+ startDate: String,
+ endDate: String,
+ contextCodes: List,
+ forceNetwork: Boolean
+ ): List {
+ if (contextCodes.isEmpty()) return emptyList()
+
+ val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork)
+
+ val allItems = coroutineScope {
+ val calendarEvents = async {
+ calendarEventApi.getCalendarEvents(
+ false,
+ CalendarEventAPI.CalendarEventType.CALENDAR.apiName,
+ startDate,
+ endDate,
+ contextCodes,
+ restParams
+ ).depaginate {
+ calendarEventApi.next(it, restParams)
+ }.dataOrThrow.toPlannerItems(PlannableType.CALENDAR_EVENT)
+ }
+
+ val calendarAssignments = async {
+ calendarEventApi.getCalendarEvents(
+ false,
+ CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName,
+ startDate,
+ endDate,
+ contextCodes,
+ restParams
+ ).depaginate {
+ calendarEventApi.next(it, restParams)
+ }.dataOrThrow.toPlannerItems(PlannableType.ASSIGNMENT)
+ }
+
+ val plannerNotes = async {
+ plannerApi.getPlannerNotes(startDate, endDate, contextCodes, restParams).depaginate {
+ plannerApi.nextPagePlannerNotes(it, restParams)
+ }.dataOrThrow.toPlannerItems()
+ }
+
+ return@coroutineScope listOf(calendarEvents, calendarAssignments, plannerNotes).awaitAll()
+ }
+
+ return allItems.flatten().sortedBy { it.plannableDate }
+ }
+
+ override suspend fun getCanvasContexts(): DataResult