diff --git a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml index 1ed267dd39..7968bc38fe 100644 --- a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml +++ b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + - + diff --git a/apps/flutter_parent/assets/html/html_wrapper.html b/apps/flutter_parent/assets/html/html_wrapper.html index 76218ec133..990e95789b 100644 --- a/apps/flutter_parent/assets/html/html_wrapper.html +++ b/apps/flutter_parent/assets/html/html_wrapper.html @@ -26,6 +26,8 @@ height: auto; margin: 0; padding: 0; + background-color: {BACKGROUND}; + color: {COLOR}; } img { @@ -65,6 +67,7 @@ a { word-wrap: break-word; + color: {LINK_COLOR} } .lti_button { @@ -81,6 +84,10 @@ font-size: 13px; margin: auto; } + + a:visited { + color: {VISITED_LINK_COLOR} + }
diff --git a/apps/flutter_parent/assets/svg/canvas-parent-login-logo-dark.svg b/apps/flutter_parent/assets/svg/canvas-parent-login-logo-dark.svg index ab80d41cfa..4a79d0ce92 100644 --- a/apps/flutter_parent/assets/svg/canvas-parent-login-logo-dark.svg +++ b/apps/flutter_parent/assets/svg/canvas-parent-login-logo-dark.svg @@ -1,10 +1,10 @@ - - - - - - + + + + + + diff --git a/apps/flutter_parent/assets/svg/canvas-parent-login-logo.svg b/apps/flutter_parent/assets/svg/canvas-parent-login-logo.svg index b595560d0c..7a6ad0883c 100644 --- a/apps/flutter_parent/assets/svg/canvas-parent-login-logo.svg +++ b/apps/flutter_parent/assets/svg/canvas-parent-login-logo.svg @@ -1,10 +1,10 @@ - - - - - - + + + + + + diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart index 0e831ca07e..53eda6d918 100644 --- a/apps/flutter_parent/lib/l10n/app_localizations.dart +++ b/apps/flutter_parent/lib/l10n/app_localizations.dart @@ -1705,4 +1705,7 @@ class AppLocalizations { String get aboutLogoSemanticsLabel => Intl.message('Instructure logo', desc: 'Semantics label for the Instructure logo on the about page'); + + 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'); } diff --git a/apps/flutter_parent/lib/l10n/res/intl_ar.arb b/apps/flutter_parent/lib/l10n/res/intl_ar.arb index fd7594f1fd..65ec9faab5 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ar.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ar.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "التنبيهات", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "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/l10n/res/intl_ca.arb b/apps/flutter_parent/lib/l10n/res/intl_ca.arb index 9f2971adb7..c153306375 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ca.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ca.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Avisos", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Logotip de l’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_cy.arb b/apps/flutter_parent/lib/l10n/res/intl_cy.arb index 3f62546257..4b8db4b546 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_cy.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_cy.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Negeseuon Hysbysu", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "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/l10n/res/intl_da.arb b/apps/flutter_parent/lib/l10n/res/intl_da.arb index 30f0d4b2ba..e1b63cd05d 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_da.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_da.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Varslinger", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure-logo", + "@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_da_instk12.arb b/apps/flutter_parent/lib/l10n/res/intl_da_instk12.arb index 02860edf64..7963c41b68 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_da_instk12.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_da_instk12.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Varslinger", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure-logo", + "@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_de.arb b/apps/flutter_parent/lib/l10n/res/intl_de.arb index 2f61a02006..a7a3052886 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_de.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_de.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Benachrichtigungen", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure-Logo", + "@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_en_AU.arb b/apps/flutter_parent/lib/l10n/res/intl_en_AU.arb index b3eed19106..1436c15ccb 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_AU.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_AU.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure logo", + "@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_en_AU_unimelb.arb b/apps/flutter_parent/lib/l10n/res/intl_en_AU_unimelb.arb index b2b3fbdd7d..3a866f3833 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_AU_unimelb.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_AU_unimelb.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure logo", + "@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_en_CY.arb b/apps/flutter_parent/lib/l10n/res/intl_en_CY.arb index 8fe52d88ba..3fa7227f2d 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_CY.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_CY.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure logo", + "@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_en_GB.arb b/apps/flutter_parent/lib/l10n/res/intl_en_GB.arb index 61612c9b10..cc92a7ec53 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_GB.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_GB.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure logo", + "@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_es.arb b/apps/flutter_parent/lib/l10n/res/intl_es.arb index b2ff089429..82a7065d8d 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_es.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_es.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alertas", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Logotipo de 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_es_ES.arb b/apps/flutter_parent/lib/l10n/res/intl_es_ES.arb index b539d7c9e3..8e0e5d6820 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_es_ES.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_es_ES.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alertas", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Logo de 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_fi.arb b/apps/flutter_parent/lib/l10n/res/intl_fi.arb index aa98e93a86..209d49a70d 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_fi.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_fi.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Hälytykset", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure-logo", + "@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_fr.arb b/apps/flutter_parent/lib/l10n/res/intl_fr.arb index 9d143b26e7..63ef1d3ea6 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_fr.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_fr.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alertes", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "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/l10n/res/intl_fr_CA.arb b/apps/flutter_parent/lib/l10n/res/intl_fr_CA.arb index e114361c87..267e9daadc 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_fr_CA.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_fr_CA.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alertes", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Logo d’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_ht.arb b/apps/flutter_parent/lib/l10n/res/intl_ht.arb index b558d309f1..e6dd7ec069 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ht.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ht.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alèt", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "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/l10n/res/intl_id.arb b/apps/flutter_parent/lib/l10n/res/intl_id.arb new file mode 100644 index 0000000000..8d63ed0338 --- /dev/null +++ b/apps/flutter_parent/lib/l10n/res/intl_id.arb @@ -0,0 +1,2670 @@ +{ + "@@last_modified": "2022-10-28T11:03:07.232972", + "alertsLabel": "Peringatan", + "@alertsLabel": { + "description": "The label for the Alerts tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "calendarLabel": "Kalender", + "@calendarLabel": { + "description": "The label for the Calendar tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "coursesLabel": "Kursus", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Students": "Tidak Ada Siswa", + "@No Students": { + "description": "Text for when an observer has no students they are observing", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Tap to show student selector": "Ketuk untuk menampilkan pemilih siswa", + "@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": "Ketuk untuk mem-pairing dengan siswa baru", + "@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": "Ketuk untuk memilih siswa ini", + "@Tap to select this student": { + "description": "Semantics label on individual students in the student switcher", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Manage Students": "Kelola Siswa", + "@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": "Bantu", + "@Help": { + "description": "Label text for the help nav drawer button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Log Out": "Logout", + "@Log Out": { + "description": "Label text for the Log Out nav drawer button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Switch Users": "Ganti Pengguna", + "@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?": "Anda yakin mau logout?", + "@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": "Kalender", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Bulan selanjutnya: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Bulan sebelumnya: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Minggu selanjutnya mulai {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Minggu sebelumnya mulai {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Bulan dari {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "perbesar", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "perkecil", + "@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} poin memungkinkan", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "No Events Today!": "Tidak Ada Acara Hari Ini!", + "@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.": "Sepertinya hari ini sangat cocok untuk beristirahat, santai, dan menyegarkan diri.", + "@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": "Terjadi kesalahan saat memuat kalender siswa Anda", + "@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.": "Ketuk untuk memfavoritkan kursus yang Anda ingin lihat di Kalender. Pilih hingga 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": "Anda hanya dapat memilih 10 kalender untuk ditampilkan", + "@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": "Anda harus memilih setidaknya satu kalender untuk ditampilkan", + "@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": "Catatan Planner", + "@Planner Note": { + "description": "Label used for notes in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Pergi ke hari ini", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Previous Logins": "Login Sebelumnya", + "@Previous Logins": { + "description": "Label for the list of previous user logins", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "canvasLogoLabel": "Logo Canvas", + "@canvasLogoLabel": { + "description": "The semantics label for the Canvas logo", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "findSchool": "Temukan Sekolah", + "@findSchool": { + "description": "Text for the find-my-school button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "findAnotherSchool": "Temukan sekolah lain", + "@findAnotherSchool": { + "description": "Text for the find-another-school button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "domainSearchInputHint": "Masukkan nama sekolah atau distrik…", + "@domainSearchInputHint": { + "description": "Input hint for the text box on the domain search screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noDomainResults": "Tidak dapat menemukan sekolah yang cocok dengan \"{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": "Bagaimana cara menemukan sekolah atau distrik saya?", + "@domainSearchHelpLabel": { + "description": "Label for the help button on the domain search screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "canvasGuides": "Panduan 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": "Dukungan 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": "Cobalah mencari nama sekolah atau distrik yang Anda coba akses, seperti “Smith Private School” atau “Smith County Schools.” Anda juga dapat memasukkan domain Canvas secara langsung, seperti “smith.instructure.com.”\n\nUntuk informasi lain tentang menemukan akun Canvas institusi Anda, Anda dapat mengunjungi {canvasGuides}, menghubungi {canvasSupport}, atau menghubungi sekolah Anda untuk mendapat bantuan.", + "@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!", + "@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.": "Kami tidak yakin apa yang terjadi, tetapi hal itu tidak baik. Hubungi kami jika ini terus terjadi.", + "@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": "Hubungi Bagian Dukungan", + "@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": "Lihat detail kesalahan", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Restart app": "Mulai ulang aplikasi", + "@Restart app": { + "description": "Label for the button that will restart the entire application", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versi aplikasi", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model perangkat", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versi 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": "Pesan kesalahan lengkap", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Inbox": "Kotak Masuk", + "@Inbox": { + "description": "Title for the Inbox screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your inbox messages.": "Terjadi kesalahan saat memuat pesan kotak masuk Anda.", + "@There was an error loading your inbox messages.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Subject": "Tanpa Subjek", + "@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.": "Tidak dapat mengambil kursus. Silakan periksa sambungan internet Anda dan coba lagi.", + "@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": "Pilih kursus ke pesan", + "@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": "Nol Kotak Masuk", + "@Inbox Zero": { + "description": "Title of the message shown when there are no inbox messages", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "You’re all caught up!": "Anda berhasil menyusul!", + "@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": "Terjadi kesalahan saat memuat penerima untuk kursus ini.", + "@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.": "Tidak dapat mengirim pesan. Periksa koneksi Anda dan coba lagi.", + "@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": "Perubahan belum disimpan", + "@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.": "Anda yakin mau menutup halaman ini? Pesan Anda yang belum dikirim akan hilang.", + "@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": "Pesan baru", + "@New message": { + "description": "Title of the new-message screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Add attachment": "Tambah lampiran", + "@Add attachment": { + "description": "Tooltip for the add-attachment button in the new-message screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Send message": "Kirim pesan", + "@Send message": { + "description": "Tooltip for the send-message button in the new-message screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select recipients": "Pilih penerima", + "@Select recipients": { + "description": "Tooltip for the button that allows users to select message recipients", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No recipients selected": "Penerima tidak dipilih", + "@No recipients selected": { + "description": "Hint displayed when the user has not selected any message recipients", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Message subject": "Subjek pesan", + "@Message subject": { + "description": "Hint text displayed in the input field for the message subject", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Message": "Pesan", + "@Message": { + "description": "Hint text displayed in the input field for the message body", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Recipients": "Penerima", + "@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.": "Gagal. Ketuk untuk opsi.", + "@Failed. Tap for options.": { + "description": "Short message shown on a message attachment when uploading has failed", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseForWhom": "untuk {studentShortName}", + "@courseForWhom": { + "description": "Describes for whom a course is for (i.e. for Bill)", + "type": "text", + "placeholders_order": [ + "studentShortName" + ], + "placeholders": { + "studentShortName": {} + } + }, + "messageLinkPostscript": "Tentang: {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": "Terjadi kesalahan saat memuat percakapan ini", + "@There was an error loading this conversation": { + "description": "Message shown when a conversation fails to load", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Reply": "Balas", + "@Reply": { + "description": "Button label for replying to a conversation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Reply All": "Balas Semua", + "@Reply All": { + "description": "Button label for replying to all conversation participants", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unknown User": "Pengguna Tidak Dikenal", + "@Unknown User": { + "description": "Label used where the user name is not known", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "me": "saya", + "@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} hingga {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} ke 1 lainnya}other{{authorName} ke {howMany} lainnya}}", + "@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} ke {recipientName} & 1 lainnya}other{{authorName} ke {recipientName} & {howMany} lainnya}}", + "@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": "Unduh", + "@Download": { + "description": "Label for the button that will begin downloading a file", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Open with another app": "Buka dengan aplikasi lain", + "@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": "Tidak ada aplikasi terinstal yang dapat membuka file ini", + "@There are no installed applications that can open this file": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsupported File": "File Tidak Didukung", + "@Unsupported File": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "This file is unsupported and can’t be viewed through the app": "File ini tidak didukung dan tidak dapat dilihat melalui aplikasi", + "@This file is unsupported and can’t be viewed through the app": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unable to play this media file": "Tidak dapat memutar file media ini", + "@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": "Tidak dapat memuat gambar ini", + "@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": "Terjadi kesalahan saat memuat file ini", + "@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": "Tidak Ada Kursus", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Kursus siswa Anda mungkin belum diterbitkan.", + "@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.": "Terjadi kesalahan ketika memuat kursus siswa Anda.", + "@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": "Tanpa Nilai", + "@No Grade": { + "description": "Message shown when there is currently no grade available for a course", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Filter by": "Filter menurut", + "@Filter by": { + "description": "Title for list of terms to filter grades by", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Grades": "Nilai", + "@Grades": { + "description": "Label for the \"Grades\" tab in course details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Syllabus": "Silabus", + "@Syllabus": { + "description": "Label for the \"Syllabus\" tab in course details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Front Page": "Halaman Depan", + "@Front Page": { + "description": "Label for the \"Front Page\" tab in course details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Summary": "Rangkuman", + "@Summary": { + "description": "Label for the \"Summary\" tab in course details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Send a message about this course": "Kirim pesan tentang kursus ini", + "@Send a message about this course": { + "description": "Accessibility hint for the course messaage floating action button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Total Grade": "Nilai Total", + "@Total Grade": { + "description": "Label for the total grade in the course", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Graded": "Dinilai", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Diserahkan", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Not Submitted": "Belum Diserahkan", + "@Not Submitted": { + "description": "Label for assignments that have not been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Late": "Terlambat", + "@Late": { + "description": "Label for assignments that have been marked late or submitted late", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Tidak Ada", + "@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": "Semua Periode Penilaian", + "@All Grading Periods": { + "description": "Label for selecting all grading periods", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Assignments": "Tidak Ada Tugas", + "@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.": "Sepertinya tugas belum dibuat di ruang ini.", + "@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.": "Terjadi kesalahan saat memuat detail rangkuman untuk kursus ini.", + "@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": "Tidak Ada Rangkuman", + "@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.": "Kursus in belum memiliki tugas atau acara kalender.", + "@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} dari {pointsPossible} poin", + "@contentDescriptionScoreOutOfPointsPossible": { + "description": "Formatted string for a student score out of the points possible", + "type": "text", + "placeholders_order": [ + "score", + "pointsPossible" + ], + "placeholders": { + "score": {}, + "pointsPossible": {} + } + }, + "gradesSubjectMessage": "Tentang: {studentName}, Nilai", + "@gradesSubjectMessage": { + "description": "The subject line for a message to a teacher regarding a student's grades", + "type": "text", + "placeholders_order": [ + "studentName" + ], + "placeholders": { + "studentName": {} + } + }, + "syllabusSubjectMessage": "Tentang: {studentName}, Silabus", + "@syllabusSubjectMessage": { + "description": "The subject line for a message to a teacher regarding a course syllabus", + "type": "text", + "placeholders_order": [ + "studentName" + ], + "placeholders": { + "studentName": {} + } + }, + "frontPageSubjectMessage": "Tentang: {studentName}, Halaman Depan", + "@frontPageSubjectMessage": { + "description": "The subject line for a message to a teacher regarding a course front page", + "type": "text", + "placeholders_order": [ + "studentName" + ], + "placeholders": { + "studentName": {} + } + }, + "assignmentSubjectMessage": "Tentang: {studentName}, Tugas - {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": "Tentang: {studentName}, Acara - {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.": "Informasi halaman tidak tersedia.", + "@There is no page information available.": { + "description": "Description for when no page information is available", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Assignment Details": "Detail Tugas", + "@Assignment Details": { + "description": "Title for the page that shows details for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} poin", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "assignmentTotalPointsAccessible": "{points} poin", + "@assignmentTotalPointsAccessible": { + "description": "Screen reader label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Due": "Batas", + "@Due": { + "description": "Label for an assignment due date", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Grade": "Kelas", + "@Grade": { + "description": "Label for the section that displays an assignment's grade", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Locked": "Terkunci", + "@Locked": { + "description": "Label for when an assignment is locked", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentLockedModule": "Tugas ini dikunci oleh modul \"{moduleName}\".", + "@assignmentLockedModule": { + "description": "The locked description when an assignment is locked by a module", + "type": "text", + "placeholders_order": [ + "moduleName" + ], + "placeholders": { + "moduleName": {} + } + }, + "Remind Me": "Ingatkan Saya", + "@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.": "Atur tanggal dan waktu untuk diberi tahu tugas spesifik ini.", + "@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…": "Anda akan diberi tahu tentang tugas ini pada…", + "@You will be notified about this assignment on…": { + "description": "Description for when a reminder is set", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Instructions": "Petunjuk", + "@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": "Kirim pesan tentang tugas ini", + "@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.": "Aplikasi ini tidak diizinkan untuk digunakan.", + "@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.": "Server yang Anda masukkan tidak diizinkan untuk aplikasi ini.", + "@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.": "Agen pengguna untuk aplikasi ini tidak diizinkan.", + "@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.": "Kami tidak dapat memverifikasi server untuk digunakan bersama aplikasi ini.", + "@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": "Pengingat", + "@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": "Notifikasi untuk pengingat tentang tugas dan acara kalender", + "@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!": "Pengingat sudah berubah!", + "@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.": "Untuk memberi Anda pengalaman yang lebih baik, kami telah memperbarui cara kerja pengingat. Anda dapat menambah pengingat dengan melihat tugas atau acara kalender dan mengetuk sakelar di bawah bagian \"Ingatkan Saya\".\n\nMohon pahami bahwa segala pengingat yang dibuat dengan versi aplikasi yang lebih tua tidak akan kompatibel dengan perubahan baru dan Anda perlu membuatnya lagi.", + "@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?": "Bukan orang tua?", + "@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": "Kami tidak dapat menemukan siswa apa pun yang terkait dengan akun ini", + "@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?": "Anda siswa atau guru?", + "@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.": "Salah satu app lain mungkin lebih cocok. Ketuk untuk membuka App 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": "Kembali ke Login", + "@Return to Login": { + "description": "Label for the button that returns the user to the login screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "STUDENT": "SISWA", + "@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": "GURU", + "@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 Student", + "@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 Teacher", + "@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": "Tanpa Peringatan", + "@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.": "Belum ada notifikasi untuk apa pun.", + "@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": "Singkirkan {alertTitle}", + "@dismissAlertLabel": { + "description": "Accessibility label to dismiss an alert", + "type": "text", + "placeholders_order": [ + "alertTitle" + ], + "placeholders": { + "alertTitle": {} + } + }, + "Course Announcement": "Pengumuman Kursus", + "@Course Announcement": { + "description": "Title for alerts when there is a course announcement", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Institution Announcement": "Pengumuman Lembaga", + "@Institution Announcement": { + "description": "Title for alerts when there is an institution announcement", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentGradeAboveThreshold": "Nilai Tugas Di Atas {threshold}", + "@assignmentGradeAboveThreshold": { + "description": "Title for alerts when an assignment grade is above the threshold value", + "type": "text", + "placeholders_order": [ + "threshold" + ], + "placeholders": { + "threshold": {} + } + }, + "assignmentGradeBelowThreshold": "Nilai Tugas Di Bawah {threshold}", + "@assignmentGradeBelowThreshold": { + "description": "Title for alerts when an assignment grade is below the threshold value", + "type": "text", + "placeholders_order": [ + "threshold" + ], + "placeholders": { + "threshold": {} + } + }, + "courseGradeAboveThreshold": "Nilai Kursus Di Atas {threshold}", + "@courseGradeAboveThreshold": { + "description": "Title for alerts when a course grade is above the threshold value", + "type": "text", + "placeholders_order": [ + "threshold" + ], + "placeholders": { + "threshold": {} + } + }, + "courseGradeBelowThreshold": "Nilai Kursus Di Bawah {threshold}", + "@courseGradeBelowThreshold": { + "description": "Title for alerts when a course grade is below the threshold value", + "type": "text", + "placeholders_order": [ + "threshold" + ], + "placeholders": { + "threshold": {} + } + }, + "Settings": "Pengaturan", + "@Settings": { + "description": "Title for the settings screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Theme": "Tema", + "@Theme": { + "description": "Label for the light/dark theme section in the settings page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Dark Mode": "Mode Gelap", + "@Dark Mode": { + "description": "Label for the button that enables dark mode", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Light Mode": "Mode Terang", + "@Light Mode": { + "description": "Label for the button that enables light mode", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "High Contrast Mode": "Mode Kontras Tinggi", + "@High Contrast Mode": { + "description": "Label for the switch that toggles high contrast mode", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Use Dark Theme in Web Content": "Gunakan Tema Gelap di Konten Web", + "@Use Dark Theme in Web Content": { + "description": "Label for the switch that toggles dark mode for webviews", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Appearance": "Tampilan", + "@Appearance": { + "description": "Label for the appearance section in the settings page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Successfully submitted!": "Berhasil diserahkan!", + "@Successfully submitted!": { + "description": "Title displayed in the grade cell for an assignment that has been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "submissionStatusSuccessSubtitle": "Tugas ini diserahkan pada {date} pukul {time} dan menunggu untuk dinilai", + "@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{Dari 1 poin}other{Dari {points} poin}}", + "@outOfPoints": { + "description": "Description for an assignment grade that has points without a current scoroe", + "type": "text", + "placeholders_order": [ + "points", + "howMany" + ], + "placeholders": { + "points": {}, + "howMany": {} + } + }, + "Excused": "Dibolehkan", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Complete": "Lengkap", + "@Complete": { + "description": "Grading status for an assignment marked as complete", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Incomplete": "Tidak lengkap", + "@Incomplete": { + "description": "Grading status for an assignment marked as incomplete", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "minus": "minus", + "@minus": { + "description": "Screen reader-friendly replacement for the \"-\" character in letter grades like \"A-\"", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "latePenalty": "Penalti terlambat ({pointsLost})", + "@latePenalty": { + "description": "Text displayed when a late penalty has been applied to the assignment", + "type": "text", + "placeholders_order": [ + "pointsLost" + ], + "placeholders": { + "pointsLost": {} + } + }, + "finalGrade": "Nilai Final: {grade}", + "@finalGrade": { + "description": "Text that displays the final grade of an assignment", + "type": "text", + "placeholders_order": [ + "grade" + ], + "placeholders": { + "grade": {} + } + }, + "Alert Settings": "Pengaturan Peringatan", + "@Alert Settings": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Alert me when…": "Ingatkan saya ketika...", + "@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": "Nilai kursus di bawah", + "@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": "Nilai kursus di atas", + "@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": "Tugas tidak ada", + "@Assignment missing": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Assignment grade below": "Nilai tugas di bawah", + "@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": "Nilai tugas di atas", + "@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": "Pengumuman Kursus", + "@Course Announcements": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Institution Announcements": "Pengumuman Institusi", + "@Institution Announcements": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Never": "Tidak Pernah", + "@Never": { + "description": "Indication that tells the user they will not receive alert notifications of a specific kind", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Grade percentage": "Persentase nilai", + "@Grade percentage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your student's alerts.": "Terjadi kesalahan ketika memuat peringatan siswa Anda.", + "@There was an error loading your student's alerts.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Must be below 100": "Harus di bawah 100", + "@Must be below 100": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mustBeBelowN": "Harus di bawah {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": "Harus di atas {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": "Pilih Warna Siswa", + "@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", + "@Electric, blue": { + "description": "Name of the Electric (blue) color", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Plum, Purple": "Plum, Purple", + "@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": "Raspberry, Red", + "@Raspberry, Red": { + "description": "Name of the Raspberry (red) color", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Fire, Orange": "Fire, Orange", + "@Fire, Orange": { + "description": "Name of the Fire (orange) color", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Shamrock, Green": "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.": "Kesalahan terjadi saat menyimpan pilihan Anda. Silakan coba lagi.", + "@An error occurred while saving your selection. Please try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "changeStudentColorLabel": "Ubah warna untuk {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": "Guru", + "@Teacher": { + "description": "Label for the Teacher enrollment type", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Student": "Siswa", + "@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": "Pengamat", + "@Observer": { + "description": "Label for the Observer enrollment type", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Use Camera": "Gunakan Kamera", + "@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": "Unggah 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": "Pilih dari Galeri", + "@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…": "Menyiapkan...", + "@Preparing…": { + "description": "Message shown while a file is being prepared to attach to a message", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Add student with…": "Tambah siswa dengan...", + "@Add student with…": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Add Student": "Tambah Siswa", + "@Add Student": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "You are not observing any students.": "Anda tidak mengamati siswa manapun.", + "@You are not observing any students.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your students.": "Terjadi kesalahan ketika memuat siswa Anda.", + "@There was an error loading your students.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Pairing Code": "Kode Pairing", + "@Pairing Code": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Students can obtain a pairing code through the Canvas website": "Siswa bisa mendapat kode pairing melalui situs web 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": "Masukkan kode pairing siswa yang diberikan kepada Anda. Jika kode pairing gagal, mungkin sudah kedaluwarsa", + "@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.": "Kode Anda salah atau sudah kedaluwarsa.", + "@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.": "Terjadi kesalahan saat mencoba membuat akun Anda, silakan hubungi sekolah Anda untuk mendapat bantuan.", + "@Something went wrong trying to create your account, please reach out to your school for assistance.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "QR Code": "Kode QR", + "@QR Code": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Students can create a QR code using the Canvas Student app on their mobile device": "Siswa dapat membuat kode QR menggunakan aplikasi Canvas Student di perangkat selulernya", + "@Students can create a QR code using the Canvas Student app on their mobile device": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Add new student": "Tambah siswa baru", + "@Add new student": { + "description": "Semantics label for the FAB on the Manage Students Screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select": "Pilih", + "@Select": { + "description": "Hint text to tell the user to choose one of two options", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "I have a Canvas account": "Saya punya akun 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": "Saya tidak punya akun 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": "Buat Akun", + "@Create Account": { + "description": "Button text for account creation confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full Name": "Nama Lengkap", + "@Full Name": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Email Address": "Alamat Email", + "@Email Address": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Password": "Kata sandi", + "@Password": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full Name…": "Nama Lengkap...", + "@Full Name…": { + "description": "hint label for inside form field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Email…": "Email...", + "@Email…": { + "description": "hint label for inside form field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Password…": "Kata sandi...", + "@Password…": { + "description": "hint label for inside form field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Please enter full name": "Silakan masukkan nama lengkap", + "@Please enter full name": { + "description": "Error message for form field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Please enter an email address": "Silakan masukkan alamat email", + "@Please enter an email address": { + "description": "Error message for form field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Please enter a valid email address": "Silakan alamat email yang valid", + "@Please enter a valid email address": { + "description": "Error message for form field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Password is required": "Kata sandi harus ada", + "@Password is required": { + "description": "Error message for form field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Password must contain at least 8 characters": "Kata sandi harus memuat setidaknya 8 karakter.", + "@Password must contain at least 8 characters": { + "description": "Error message for form field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "qrCreateAccountTos": "Dengan mengetuk ‘Buat Akun’, Anda menyetujui {termsOfService} dan mengakui {privacyPolicy}", + "@qrCreateAccountTos": { + "description": "The text show on the account creation screen", + "type": "text", + "placeholders_order": [ + "termsOfService", + "privacyPolicy" + ], + "placeholders": { + "termsOfService": {}, + "privacyPolicy": {} + } + }, + "Terms of Service": "Ketentuan Layanan", + "@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": "Kebijakan Privasi", + "@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": "Lihat Kebijakan Privasi", + "@View the Privacy Policy": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Already have an account? ": "Sudah memiliki akun? ", + "@Already have an account? ": { + "description": "Part of multiline text span, includes AccountSignIn1-2, in that order", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Sign In": "Masuk", + "@Sign In": { + "description": "Part of multiline text span, includes AccountSignIn1-2, in that order", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Hide Password": "Sembunyikan Kata Sandi", + "@Hide Password": { + "description": "content description for password hide button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Show Password": "Tampilkan Kata Sandi", + "@Show Password": { + "description": "content description for password show button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Terms of Service Link": "Tautan Ketentuan Layanan", + "@Terms of Service Link": { + "description": "content description for terms of service link", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Privacy Policy Link": "Tautan Kebijakan Privasi", + "@Privacy Policy Link": { + "description": "content description for privacy policy link", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Event": "Acara", + "@Event": { + "description": "Title for the event details screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Tanggal", + "@Date": { + "description": "Label for the event date", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Location": "Lokasi", + "@Location": { + "description": "Label for the location information", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Location Specified": "Lokasi Tidak Ditetapkan", + "@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.": "Atur tanggal dan waktu untuk diberi tahu tentang acara ini.", + "@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…": "Anda akan diberi tahu tentang acara ini pada…", + "@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": "Bagikan Cinta Anda untuk Aplikasi", + "@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": "Beri tahu kami bagian favorit Anda dari aplikasi", + "@Tell us about your favorite parts of the app": { + "description": "Description for option to open the app store", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Legal": "Hukum", + "@Legal": { + "description": "Label for legal information option", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Privacy policy, terms of use, open source": "Kebijakan privasi, ketentuan penggunaan, 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]": "Ide untuk aplikasi Canvas Parent [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:": "Informasi berikut akan membantu kami memahami ide Anda lebih baik:", + "@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:", + "@Domain:": { + "description": "The label for the Canvas domain of the logged in user", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "User ID:": "ID Pengguna:", + "@User ID:": { + "description": "The label for the Canvas user ID of the logged in user", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Email:": "Email:", + "@Email:": { + "description": "The label for the eamil of the logged in user", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Locale:": "Lokasi:", + "@Locale:": { + "description": "The label for the locale of the logged in user", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Terms of Use": "Ketentuan Penggunaan", + "@Terms of Use": { + "description": "Label for the terms of use", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Canvas on GitHub": "Canvas di 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": "Ada masalah saat memuat Ketentuan Penggunaan", + "@There was a problem loading the Terms of Use": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device": "Perangkat", + "@Device": { + "description": "Label used for device manufacturer/model in the error report", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "OS Version": "Versi OS", + "@OS Version": { + "description": "Label used for device operating system version in the error report", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Version Number": "Nomor Versi", + "@Version Number": { + "description": "Label used for the app version number in the error report", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Report A Problem": "Laporkan Masalah", + "@Report A Problem": { + "description": "Title used for generic dialog to report problems", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Subject": "Subjek", + "@Subject": { + "description": "Label used for Subject text field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "A subject is required.": "Deskripsi harus ada.", + "@A subject is required.": { + "description": "Error shown when the subject field is empty", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "An email address is required.": "Alamat email harus ada.", + "@An email address is required.": { + "description": "Error shown when the email field is empty", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Deskripsi", + "@Description": { + "description": "Label used for Description text field", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "A description is required.": "Deskripsi harus ada.", + "@A description is required.": { + "description": "Error shown when the description field is empty", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "How is this affecting you?": "Bagaimana ini berpengaruh pada Anda?", + "@How is this affecting you?": { + "description": "Label used for the dropdown to select how severe the issue is", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "send": "kirim", + "@send": { + "description": "Label used for send button when reporting a problem", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Just a casual question, comment, idea, suggestion…": "Hanya pertanyaan biasa, komentar, ide, saran...", + "@Just a casual question, comment, idea, suggestion…": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "I need some help but it's not urgent.": "Saya butuh bantuan tetapi tidak mendesak.", + "@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.": "Ada yang terputus tetapi bisa saya cari cara untuk mendapat apa yang saya butuhkan.", + "@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.": "Saya tidak bisa melakukan apa pun sampai saya mendapat info dari Anda.", + "@I can't get things done until I hear back from you.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "EXTREME CRITICAL EMERGENCY!!": "DARURAT KRITIKAL EKSTREM!!", + "@EXTREME CRITICAL EMERGENCY!!": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Not Graded": "Tidak Dinilai", + "@Not Graded": { + "description": "Description for an assignment has not been graded.", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Login flow: Normal": "Alur login: Normal", + "@Login flow: Normal": { + "description": "Description for the normal login flow", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Login flow: Canvas": "Alur login: Canvas", + "@Login flow: Canvas": { + "description": "Description for the Canvas login flow", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Login flow: Site Admin": "Alur login: Admin Situs", + "@Login flow: Site Admin": { + "description": "Description for the Site Admin login flow", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Login flow: Skip mobile verify": "Alur login: Lewatkan verifikasi seluler", + "@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": "Bertindak sebagai Pengguna", + "@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 Bertindak sebagai Pengguna", + "@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": "Anda bertindak sebagai {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” (Bertindak Sebagai) pada dasarnya adalah melakukan login sebagai pengguna ini tanpa kata sandi. Anda akan dapat melakukan tindakan apa pun jika Anda adalah pengguna ini, dan dari sudut pandang pengguna lain, ini seakan-akan pengguna ini melakukannya. Namun, log audit mencatat bahwa Anda lah orang yang melakukan tindakan atas nama pengguna ini.", + "@\"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", + "@Domain": { + "description": "Text field hint for domain url input", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "You must enter a valid domain": "Anda harus memasukkan domain yang valid", + "@You must enter a valid domain": { + "description": "Message displayed for domain input error", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "User ID": "ID Pengguna", + "@User ID": { + "description": "Text field hint for user ID input", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "You must enter a user id": "Anda harus memasukkan id pengguna", + "@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.": "Terjadi kesalahan saat mencoba bertindak sebagai pengguna ini. Silakan cek Domain dan ID Pengguna dan coba lagi.", + "@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": "Anda akan berhenti bertindak sebagai {userName} dan kembali ke akun asli Anda.", + "@endMasqueradeMessage": { + "description": "Confirmation message displayed when the user wants to stop acting (masquerading) as another user", + "type": "text", + "placeholders_order": [ + "userName" + ], + "placeholders": { + "userName": {} + } + }, + "endMasqueradeLogoutMessage": "Anda akan berhenti bertindak sebagai {userName} dan logout.", + "@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?": "Bagaimana kabarnya?", + "@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": "Jangan tampilkan lagi", + "@Don't show again": { + "description": "Button to prevent the rating dialog from showing again.", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "What can we do better?": "Apa yang dapat kami tingkatkan?", + "@What can we do better?": { + "description": "Hint text for providing a comment with the rating.", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Send Feedback": "Kirim Umpan Balik", + "@Send Feedback": { + "description": "Button to send rating with feedback", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "ratingDialogEmailSubject": "Saran untuk 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} bintang}other{{position} bintang}}", + "@starRating": { + "description": "Accessibility label for the 1 stars to 5 stars rating", + "type": "text", + "placeholders_order": [ + "position" + ], + "placeholders": { + "position": { + "example": 1 + } + } + }, + "Student Pairing": "Pairing Siswa", + "@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": "Buka 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.": "Anda harus membuka aplikasi Canvas Student Anda untuk melanjutkan. Pergilah ke Menu Utama > Pengaturan > Pairing dengan Pengamat dan pindai kode QR yang Anda lihat di sana.", + "@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": "Tangkapan layar yang menampilkan lokasi pembuatan pairing kode QR di aplikasi Canvas Student", + "@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": "Kode QR Kedaluwarsa", + "@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.": "Kode QR yang Anda pindai mungkin telah kedaluwarsa. Muat ulang kode di perangkat siswa dan coba lagi.", + "@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.": "Terjadi kesalahan jaringan saat menambah siswa ini. Periksa koneksi Anda dan coba lagi.", + "@A network error occurred when adding this student. Check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Invalid QR Code": "Kode QR Tidak Valid", + "@Invalid QR Code": { + "description": "Error title shown when the user scans an invalid QR code", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Incorrect Domain": "Domain Salah", + "@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.": "Siswa yang Anda coba tambah ada di sekolah lain. Masuk atau buat akun dengan sekolah itu untuk memindai kode ini.", + "@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": "Izin Kamera", + "@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.": "Ini akan menghapus pairing dan menghapus semua pendaftaran untuk siswa ini dari akun Anda.", + "@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.": "Ada masalah saat menghapus siswa ini dari akun Anda. Silakan periksa sambungan internet Anda dan coba lagi.", + "@There was a problem removing this student from your account. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Batal", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "next": "Berikutnya", + "@next": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "ok": "OK", + "@ok": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ya", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Tidak", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Coba lagi", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Hapus", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Selesai", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Refresh": "Segarkan Ulang", + "@Refresh": { + "description": "Label for button to refresh data from the web", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View Description": "Lihat Deskripsi", + "@View Description": { + "description": "Button to view the description for an event or assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "expanded": "diperbesar", + "@expanded": { + "description": "Description for the accessibility reader for list groups that are expanded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapsed": "diperkecil", + "@collapsed": { + "description": "Description for the accessibility reader for list groups that are expanded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "An unexpected error occurred": "Terjadi kesalahan yang tidak terduga", + "@An unexpected error occurred": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No description": "Tanpa deskripsi", + "@No description": { + "description": "Message used when the assignment has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Launch External Tool": "Luncurkan Alat Eksternal", + "@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.": "Interaksi di halaman ini dibatasi oleh institusi Anda.", + "@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} di {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Batas waktu {date} pada {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "No Due Date": "Tidak Ada Batas Waktu", + "@No Due Date": { + "description": "Label for assignments that do not have a due date", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Filter": "Filter", + "@Filter": { + "description": "Label for buttons to filter what items are visible", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "unread": "belum dibaca", + "@unread": { + "description": "Label for things that are marked as unread", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "unreadCount": "{count} belum dibaca", + "@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": "Terjadi kesalahan saat memuat pengumuman ini", + "@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": "Kesalahan jaringan", + "@Network error": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Under Construction": "Sedang Dibuat", + "@Under Construction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We are currently building this feature for your viewing pleasure.": "Kami saat ini sedang membangun fitur ini untuk memudahkan Anda.", + "@We are currently building this feature for your viewing pleasure.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Request Login Help Button": "Tombol Minta Bantuan Login", + "@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": "Minta Bantuan Login", + "@Request Login Help": { + "description": "Title of help dialog for a login help request", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "I'm having trouble logging in": "Saya kesulitan login", + "@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": "Kesalahan terjadi saat mencoba menampilkan tautan ini.", + "@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.": "Kami tidak dapat menampilkan tautan ini, mungkin milik institusi tempat Anda saat ini tidak login kepadanya.", + "@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": "Kesalahan Tautan", + "@Link Error": { + "description": "Title for error page shown when clicking a link", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Open In Browser": "Buka di 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.": "Anda akan menemukan kode QR di web di profil akun Anda. Klik 'QR untuk Login Seluler' di dalam daftar.", + "@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": "Temukan Kode QR", + "@Locate QR Code": { + "description": "Text for qr login button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Please scan a QR code generated by Canvas": "Silakan pindai kode QR yang dibuat oleh 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.": "Terjadi kesalahan saat login. Silakan buat Kode QR lain dan coba lagi.", + "@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": "Tangkapan layar yang menampilkan lokasi pembuatan kode QR di browser", + "@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": "Pemindaian QR membutuhkan akses kamera", + "@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": "Item yang dikaitkan tidak lagi tersedia", + "@The linked item is no longer available": { + "description": "error message when the alert could no be opened", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Message sent": "Pesan terkirim", + "@Message sent": { + "description": "confirmation message on the screen when the user succesfully sends a message", + "type": "text", + "placeholders_order": [], + "placeholders": {} + } +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_is.arb b/apps/flutter_parent/lib/l10n/res/intl_is.arb index 7a09f7f413..611e1defc6 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_is.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_is.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Viðvaranir", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Merki 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_it.arb b/apps/flutter_parent/lib/l10n/res/intl_it.arb index 2b0d6ee265..a3dcc59070 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_it.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_it.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Avvisi", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "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/l10n/res/intl_ja.arb b/apps/flutter_parent/lib/l10n/res/intl_ja.arb index 9b16649315..4a6e294d49 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ja.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ja.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "アラート", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "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/l10n/res/intl_mi.arb b/apps/flutter_parent/lib/l10n/res/intl_mi.arb index 488c681b32..8f5f31e169 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_mi.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_mi.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "He whakamataara", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Tohu 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_ms.arb b/apps/flutter_parent/lib/l10n/res/intl_ms.arb index 2e83b495d5..3efac2b1a0 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ms.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ms.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Isyarat", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "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/l10n/res/intl_nb.arb b/apps/flutter_parent/lib/l10n/res/intl_nb.arb index c272e85eba..be343afbd5 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_nb.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_nb.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Varsler", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure-logo", + "@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_nb_instk12.arb b/apps/flutter_parent/lib/l10n/res/intl_nb_instk12.arb index 75c8d09084..85d1ddabb1 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_nb_instk12.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_nb_instk12.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Varsler", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure-logo", + "@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_nl.arb b/apps/flutter_parent/lib/l10n/res/intl_nl.arb index 27668187aa..6faf9e6f07 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_nl.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_nl.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Waarschuwingen", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "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/l10n/res/intl_pl.arb b/apps/flutter_parent/lib/l10n/res/intl_pl.arb index dcc306e354..07affc143d 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_pl.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_pl.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alerty", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "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/l10n/res/intl_pt_BR.arb b/apps/flutter_parent/lib/l10n/res/intl_pt_BR.arb index 58ba0f7119..6a389c22ec 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_pt_BR.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_pt_BR.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alertas", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Logotipo 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_pt_PT.arb b/apps/flutter_parent/lib/l10n/res/intl_pt_PT.arb index 6505af0e5a..2c6eaab49a 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_pt_PT.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_pt_PT.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alertas", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Logótipo da 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_ru.arb b/apps/flutter_parent/lib/l10n/res/intl_ru.arb index 1f5673f194..8a19ee89e1 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ru.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ru.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Предупреждения", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "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/l10n/res/intl_sv.arb b/apps/flutter_parent/lib/l10n/res/intl_sv.arb index 2520c94ae0..cf0f525594 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_sv.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_sv.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Notiser", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure-logotyp", + "@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_sv_instk12.arb b/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb index d9196a45bb..33bd40ccb6 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Notiser", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure-logotyp", + "@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_th.arb b/apps/flutter_parent/lib/l10n/res/intl_th.arb index 4b9c774051..e83fce0a50 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_th.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_th.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "แจ้งเตือน", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "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/l10n/res/intl_vi.arb b/apps/flutter_parent/lib/l10n/res/intl_vi.arb index 466548adb1..13ce5f0ccf 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_vi.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_vi.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Cảnh Báo", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "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/l10n/res/intl_zh.arb b/apps/flutter_parent/lib/l10n/res/intl_zh.arb index bcb61c5d1d..ccf2a3c7c1 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_zh.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_zh.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "警告", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "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/l10n/res/intl_zh_HK.arb b/apps/flutter_parent/lib/l10n/res/intl_zh_HK.arb index c19d37b2ac..3cb50f9e11 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_zh_HK.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_zh_HK.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "提醒", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "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/main.dart b/apps/flutter_parent/lib/main.dart index deae3fbbe4..1ee693d05a 100644 --- a/apps/flutter_parent/lib/main.dart +++ b/apps/flutter_parent/lib/main.dart @@ -33,7 +33,6 @@ import 'package:flutter_parent/utils/notification_util.dart'; import 'package:flutter_parent/utils/old_app_migration.dart'; import 'package:flutter_parent/utils/remote_config_utils.dart'; import 'package:flutter_parent/utils/service_locator.dart'; -import 'package:webview_flutter/webview_flutter.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; void main() async { diff --git a/apps/flutter_parent/lib/models/course_grade.dart b/apps/flutter_parent/lib/models/course_grade.dart index dfe9b14b76..681be6364f 100644 --- a/apps/flutter_parent/lib/models/course_grade.dart +++ b/apps/flutter_parent/lib/models/course_grade.dart @@ -69,7 +69,7 @@ class CourseGrade { /// If the course contains no valid current grade or score, this flag will be true. This is usually represented in the /// UI with "N/A". bool noCurrentGrade() => - _getCurrentScore() == null && (currentGrade() == null || currentGrade()!.contains('N/A') || currentGrade()!.isEmpty); + currentScore() == null && (currentGrade() == null || currentGrade()!.contains('N/A') || currentGrade()!.isEmpty); bool _hasActiveGradingPeriod() => !_forceAllPeriods && 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 1a3ed256ed..9043221d75 100644 --- a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart +++ b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart @@ -26,9 +26,14 @@ import 'package:flutter_parent/utils/common_widgets/web_view/html_description_ti 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'; +import 'package:flutter_parent/utils/notification_util.dart'; +import 'package:flutter_parent/utils/permission_handler.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../../utils/veneers/flutter_snackbar_veneer.dart'; class AssignmentDetailsScreen extends StatefulWidget { final String courseId; @@ -72,6 +77,8 @@ class _AssignmentDetailsScreenState extends State { Future _loadReminder() => _interactor.loadReminder(widget.assignmentId); + PermissionHandler get _permissionHandler => locator(); + @override Widget build(BuildContext context) { return FutureBuilder( @@ -313,6 +320,15 @@ class _AssignmentDetailsScreenState extends State { DateTime? date; TimeOfDay? time; + final permissionResult = await _permissionHandler.checkPermissionStatus(Permission.scheduleExactAlarm); + if (permissionResult != PermissionStatus.granted) { + final permissionGranted = await locator().requestScheduleExactAlarmPermission(); + if (permissionGranted != true) { + locator().showSnackBar(context, L10n(context).needToEnablePermission); + return; + } + } + date = await showDatePicker( context: context, initialDate: initialDate, diff --git a/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart b/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart index 531152fe15..68756af170 100644 --- a/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart +++ b/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart @@ -75,10 +75,12 @@ class _TermsOfUseScreenState extends State { // Content return WebView( - darkMode: ParentTheme.of(context)?.isWebViewDarkMode, onWebViewCreated: (controller) { - controller.loadHtml(snapshot.data!.content!, horizontalPadding: 16); - }, + controller.loadHtml(snapshot.data!.content!, + horizontalPadding: 16, + darkMode: + ParentTheme.of(context)?.isWebViewDarkMode ?? false); + }, navigationDelegate: _handleNavigation ); }, diff --git a/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart b/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart index 1b8d04272c..dd8f5e7c08 100644 --- a/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart +++ b/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart @@ -134,7 +134,6 @@ class _WebLoginScreenState extends State { navigationDelegate: (request) => _navigate(context, request, verifyResult), javascriptMode: JavascriptMode.unrestricted, - darkMode: ParentTheme.of(context)?.isWebViewDarkMode, userAgent: ApiPrefs.getUserAgent(), onPageFinished: (url) => _pageFinished(url, verifyResult), onPageStarted: (url) => _pageStarted(url), @@ -243,16 +242,15 @@ class _WebLoginScreenState extends State { /// Load the authenticated url with any necessary cookies void _loadAuthUrl() async { _showLoadingState(); - CookieManager().clearCookies(); + final cookieManager = CookieManager(); + cookieManager.clearCookies(); if (widget.loginFlow == LoginFlow.siteAdmin) { - await _controller?.setAcceptThirdPartyCookies(true); if (_domain.contains('.instructure.com')) { - String cookie = 'canvas_sa_delegated=1;domain=.instructure.com;path=/;'; - await _controller?.setCookie(_domain, cookie); - await _controller?.setCookie('.instructure.com', cookie); + cookieManager.setCookie(WebViewCookie(name: 'canvas_sa_delegated', value: '1', domain: _domain)); + cookieManager.setCookie(WebViewCookie(name: 'canvas_sa_delegated', value: '1', domain: '.instructure.com')); } else { - await _controller?.setCookie(_domain, 'canvas_sa_delegated=1'); + cookieManager.setCookie(WebViewCookie(name: 'canvas_sa_delegated', value: '1', domain: _domain)); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart index 1a1a21ee17..4052a2549f 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart @@ -230,14 +230,15 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO if (_content != widgetContent) { _height = widget.initialHeight; _content = widgetContent!; - _controller?.loadHtml(_content, horizontalPadding: widget.horizontalPadding); + _controller?.loadHtml(_content, + horizontalPadding: widget.horizontalPadding, + darkMode: ParentTheme.of(context)?.isWebViewDarkMode ?? false); } Widget child = WebView( javascriptMode: JavascriptMode.unrestricted, onPageFinished: _handlePageLoaded, onWebViewCreated: _handleWebViewCreated, - darkMode: ParentTheme.of(context)?.isWebViewDarkMode == true, navigationDelegate: _handleNavigation, gestureRecognizers: _webViewGestures(), javascriptChannels: _webViewChannels(), @@ -260,7 +261,10 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO } void _handleWebViewCreated(WebViewController webViewController) async { - webViewController.loadHtml(_content, baseUrl: ApiPrefs.getDomain(), horizontalPadding: widget.horizontalPadding); + webViewController.loadHtml(_content, + baseUrl: ApiPrefs.getDomain(), + horizontalPadding: widget.horizontalPadding, + darkMode: ParentTheme.of(context)?.isWebViewDarkMode ?? false); _controller = webViewController; } 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 7bcbb880d5..cc186be9dc 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 @@ -48,7 +48,6 @@ class _SimpleWebViewScreenState extends State { body: WebView( javascriptMode: JavascriptMode.unrestricted, userAgent: ApiPrefs.getUserAgent(), - darkMode: ParentTheme.of(context)?.isWebViewDarkMode, gestureRecognizers: Set()..add(Factory(() => WebViewGestureRecognizer())), navigationDelegate: _handleNavigation, onWebViewCreated: (controller) { diff --git a/apps/flutter_parent/lib/utils/notification_util.dart b/apps/flutter_parent/lib/utils/notification_util.dart index 5ea59291ad..3c1307a516 100644 --- a/apps/flutter_parent/lib/utils/notification_util.dart +++ b/apps/flutter_parent/lib/utils/notification_util.dart @@ -33,20 +33,18 @@ class NotificationUtil { static const notificationChannelReminders = 'com.instructure.parentapp/reminders'; - static FlutterLocalNotificationsPlugin? _plugin; + static AndroidFlutterLocalNotificationsPlugin? _plugin; @visibleForTesting - static initForTest(FlutterLocalNotificationsPlugin plugin) { + static initForTest(AndroidFlutterLocalNotificationsPlugin plugin) { _plugin = plugin; } static Future init(Completer? appCompleter) async { - var initializationSettings = InitializationSettings( - android: AndroidInitializationSettings('ic_notification_canvas_logo') - ); + var initializationSettings = AndroidInitializationSettings('ic_notification_canvas_logo'); if (_plugin == null) { - _plugin = FlutterLocalNotificationsPlugin(); + _plugin = AndroidFlutterLocalNotificationsPlugin(); } await _plugin!.initialize( @@ -104,12 +102,10 @@ class NotificationUtil { ..type = NotificationPayloadType.reminder ..data = json.encode(serialize(reminder))); - final notificationDetails = NotificationDetails( - android: AndroidNotificationDetails( + final notificationDetails = AndroidNotificationDetails( notificationChannelReminders, l10n.remindersNotificationChannelName, channelDescription: l10n.remindersNotificationChannelDescription - ) ); if (reminder.type == Reminder.TYPE_ASSIGNMENT) { @@ -130,8 +126,8 @@ class NotificationUtil { body, date, notificationDetails, - payload: json.encode(serialize(payload)), - uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, + scheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + payload: json.encode(serialize(payload)) ); } @@ -140,4 +136,8 @@ class NotificationUtil { Future deleteNotifications(List ids) async { for (int id in ids) await _plugin!.cancel(id); } + + Future requestScheduleExactAlarmPermission() async { + return await _plugin?.requestExactAlarmsPermission(); + } } diff --git a/apps/flutter_parent/lib/utils/web_view_utils.dart b/apps/flutter_parent/lib/utils/web_view_utils.dart index cd46d97c27..ff55e34412 100644 --- a/apps/flutter_parent/lib/utils/web_view_utils.dart +++ b/apps/flutter_parent/lib/utils/web_view_utils.dart @@ -30,15 +30,20 @@ extension WebViewUtils on WebViewController { String? html, { String? baseUrl, Map? headers, - double horizontalPadding = 0}) + double horizontalPadding = 0, + bool darkMode = false}) async { String fileText = await rootBundle.loadString('assets/html/html_wrapper.html'); html = _applyWorkAroundForDoubleSlashesAsUrlSource(html); html = _addProtocolToLinks(html); html = _checkForMathTags(html); html = fileText.replaceAll('{CANVAS_CONTENT}', html); + html = html.replaceAll('{BACKGROUND}', darkMode ? '#000000' : '#ffffff'); + html = html.replaceAll('{COLOR}', darkMode ? '#ffffff' : '#000000'); + html = html.replaceAll('{LINK_COLOR}', darkMode ? '#1283C4' : '#0374B5'); + html = html.replaceAll('{VISITED_LINK_COLOR}', darkMode ? '#C74BAF' : '#BF32A4'); html = html.replaceAll('{PADDING}', horizontalPadding.toString()); - this.loadData(baseUrl, html, 'text/html', 'utf-8'); + this.loadHtmlString(html, baseUrl: baseUrl); } /** diff --git a/apps/flutter_parent/plugins/webview_flutter/LICENSE b/apps/flutter_parent/plugins/webview_flutter/LICENSE deleted file mode 100644 index ad33cf3c3e..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2018 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/apps/flutter_parent/plugins/webview_flutter/android/build.gradle b/apps/flutter_parent/plugins/webview_flutter/android/build.gradle deleted file mode 100644 index 9e807667a5..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -group 'io.flutter.plugins.webviewflutter' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath "com.android.tools.build:gradle:7.4.2" - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 33 - - defaultConfig { - minSdkVersion 21 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } - - dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'androidx.webkit:webkit:1.7.0' - } - - namespace 'io.flutter.plugins.webviewflutter' -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/settings.gradle b/apps/flutter_parent/plugins/webview_flutter/android/settings.gradle deleted file mode 100644 index 5be7a4b4c6..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'webview_flutter' diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/AndroidManifest.xml b/apps/flutter_parent/plugins/webview_flutter/android/src/main/AndroidManifest.xml deleted file mode 100644 index a087f2c75c..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java deleted file mode 100644 index 1273e73496..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java +++ /dev/null @@ -1,143 +0,0 @@ -package io.flutter.plugins.webviewflutter; - -import static android.hardware.display.DisplayManager.DisplayListener; - -import android.annotation.TargetApi; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.util.Log; -import java.lang.reflect.Field; -import java.util.ArrayList; - -/** - * Works around an Android WebView bug by filtering some DisplayListener invocations. - * - *

Older Android WebView versions had assumed that when {@link DisplayListener#onDisplayChanged} - * is invoked, the display ID it is provided is of a valid display. However it turns out that when a - * display is removed Android may call onDisplayChanged with the ID of the removed display, in this - * case the Android WebView code tries to fetch and use the display with this ID and crashes with an - * NPE. - * - *

This issue was fixed in the Android WebView code in - * https://chromium-review.googlesource.com/517913 which is available starting WebView version - * 58.0.3029.125 however older webviews in the wild still have this issue. - * - *

Since Flutter removes virtual displays whenever a platform view is resized the webview crash - * is more likely to happen than other apps. And users were reporting this issue see: - * https://github.com/flutter/flutter/issues/30420 - * - *

This class works around the webview bug by unregistering the WebView's DisplayListener, and - * instead registering its own DisplayListener which delegates the callbacks to the WebView's - * listener unless it's a onDisplayChanged for an invalid display. - * - *

I did not find a clean way to get a handle of the WebView's DisplayListener so I'm using - * reflection to fetch all registered listeners before and after initializing a webview. In the - * first initialization of a webview within the process the difference between the lists is the - * webview's display listener. - */ -@TargetApi(Build.VERSION_CODES.KITKAT) -class DisplayListenerProxy { - private static final String TAG = "DisplayListenerProxy"; - - private ArrayList listenersBeforeWebView; - - /** Should be called prior to the webview's initialization. */ - void onPreWebViewInitialization(DisplayManager displayManager) { - listenersBeforeWebView = yoinkDisplayListeners(displayManager); - } - - /** Should be called after the webview's initialization. */ - void onPostWebViewInitialization(final DisplayManager displayManager) { - final ArrayList webViewListeners = yoinkDisplayListeners(displayManager); - // We recorded the list of listeners prior to initializing webview, any new listeners we see - // after initializing the webview are listeners added by the webview. - webViewListeners.removeAll(listenersBeforeWebView); - - if (webViewListeners.isEmpty()) { - // The Android WebView registers a single display listener per process (even if there - // are multiple WebView instances) so this list is expected to be non-empty only the - // first time a webview is initialized. - // Note that in an add2app scenario if the application had instantiated a non Flutter - // WebView prior to instantiating the Flutter WebView we are not able to get a reference - // to the WebView's display listener and can't work around the bug. - // - // This means that webview resizes in add2app Flutter apps with a non Flutter WebView - // running on a system with a webview prior to 58.0.3029.125 may crash (the Android's - // behavior seems to be racy so it doesn't always happen). - return; - } - - for (DisplayListener webViewListener : webViewListeners) { - // Note that while DisplayManager.unregisterDisplayListener throws when given an - // unregistered listener, this isn't an issue as the WebView code never calls - // unregisterDisplayListener. - displayManager.unregisterDisplayListener(webViewListener); - - // We never explicitly unregister this listener as the webview's listener is never - // unregistered (it's released when the process is terminated). - displayManager.registerDisplayListener( - new DisplayListener() { - @Override - public void onDisplayAdded(int displayId) { - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayAdded(displayId); - } - } - - @Override - public void onDisplayRemoved(int displayId) { - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayRemoved(displayId); - } - } - - @Override - public void onDisplayChanged(int displayId) { - if (displayManager.getDisplay(displayId) == null) { - return; - } - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayChanged(displayId); - } - } - }, - null); - } - } - - @SuppressWarnings({"unchecked", "PrivateApi"}) - private static ArrayList yoinkDisplayListeners(DisplayManager displayManager) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // We cannot use reflection on Android P, but it shouldn't matter as it shipped - // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was - // fixed in 61.0.3116.0. - return new ArrayList<>(); - } - try { - Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal"); - displayManagerGlobalField.setAccessible(true); - Object displayManagerGlobal = displayManagerGlobalField.get(displayManager); - Field displayListenersField = - displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners"); - displayListenersField.setAccessible(true); - ArrayList delegates = - (ArrayList) displayListenersField.get(displayManagerGlobal); - - Field listenerField = null; - ArrayList listeners = new ArrayList<>(); - for (Object delegate : delegates) { - if (listenerField == null) { - listenerField = delegate.getClass().getField("mListener"); - listenerField.setAccessible(true); - } - DisplayManager.DisplayListener listener = - (DisplayManager.DisplayListener) listenerField.get(delegate); - listeners.add(listener); - } - return listeners; - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.w(TAG, "Could not extract WebView's display listeners. " + e); - return new ArrayList<>(); - } - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java deleted file mode 100644 index 86b4fd412a..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; - -class FlutterCookieManager implements MethodCallHandler { - private final MethodChannel methodChannel; - - FlutterCookieManager(BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "clearCookies": - clearCookies(result); - break; - default: - result.notImplemented(); - } - } - - void dispose() { - methodChannel.setMethodCallHandler(null); - } - - private static void clearCookies(final Result result) { - CookieManager cookieManager = CookieManager.getInstance(); - final boolean hasCookies = cookieManager.hasCookies(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies( - new ValueCallback() { - @Override - public void onReceiveValue(Boolean value) { - result.success(hasCookies); - } - }); - } else { - cookieManager.removeAllCookie(); - result.success(hasCookies); - } - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java deleted file mode 100644 index cd0e518ea6..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ /dev/null @@ -1,477 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebStorage; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.webkit.CookieManager; -import androidx.webkit.WebSettingsCompat; -import androidx.webkit.WebViewFeature; -import androidx.annotation.NonNull; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.platform.PlatformView; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import android.util.Log; - - -public class FlutterWebView implements PlatformView, MethodCallHandler { - private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final InputAwareWebView webView; - private final MethodChannel methodChannel; - private final FlutterWebViewClient flutterWebViewClient; - private final Handler platformThreadHandler; - - // Verifies that a url opened by `Window.open` has a secure url. - private class FlutterWebChromeClient extends WebChromeClient { - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient webViewClient = - new WebViewClient() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - final String url = request.getUrl().toString(); - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, request)) { - webView.loadUrl(url); - } - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, url)) { - webView.loadUrl(url); - } - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(webViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @SuppressWarnings("unchecked") - FlutterWebView( - final Context context, - BinaryMessenger messenger, - int id, - Map params, - View containerView) { - - DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - displayListenerProxy.onPreWebViewInitialization(displayManager); - webView = new InputAwareWebView(context, containerView); - displayListenerProxy.onPostWebViewInitialization(displayManager); - - platformThreadHandler = new Handler(context.getMainLooper()); - // Allow local storage. - webView.getSettings().setDomStorageEnabled(true); - webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); - - // Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679. - webView.getSettings().setSupportMultipleWindows(true); - webView.setWebChromeClient(new FlutterWebChromeClient()); - - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); - Map settings = (Map) params.get("settings"); - if (settings != null) applySettings(settings); - - if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); - if (names != null) registerJavaScriptChannelNames(names); - } - - Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); - if (autoMediaPlaybackPolicy != null) updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); - if (params.containsKey("userAgent")) { - String userAgent = (String) params.get("userAgent"); - updateUserAgent(userAgent); - } - if (params.containsKey("initialUrl")) { - String url = (String) params.get("initialUrl"); - webView.loadUrl(url); - } - } - - @Override - public View getView() { - return webView; - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionUnlocked() { - webView.unlockInputConnection(); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionLocked() { - webView.lockInputConnection(); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewAttached(View flutterView) { - webView.setContainerView(flutterView); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewDetached() { - webView.setContainerView(null); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "loadUrl": - loadUrl(methodCall, result); - break; - case "loadData": - loadData(methodCall, result); - break; - case "updateSettings": - updateSettings(methodCall, result); - break; - case "canGoBack": - canGoBack(result); - break; - case "canGoForward": - canGoForward(result); - break; - case "goBack": - goBack(result); - break; - case "goForward": - goForward(result); - break; - case "reload": - reload(result); - break; - case "currentUrl": - currentUrl(result); - break; - case "evaluateJavascript": - evaluateJavaScript(methodCall, result); - break; - case "addJavascriptChannels": - addJavaScriptChannels(methodCall, result); - break; - case "removeJavascriptChannels": - removeJavaScriptChannels(methodCall, result); - break; - case "clearCache": - clearCache(result); - break; - case "getTitle": - getTitle(result); - break; - case "scrollTo": - scrollTo(methodCall, result); - break; - case "scrollBy": - scrollBy(methodCall, result); - break; - case "getScrollX": - getScrollX(result); - break; - case "getScrollY": - getScrollY(result); - break; - case "setAcceptThirdPartyCookies": - setAcceptThirdPartyCookies(methodCall, result); - break; - case "setCookie": - setCookie(methodCall, result); - break; - default: - result.notImplemented(); - } - } - - @SuppressWarnings("unchecked") - private void loadUrl(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - String url = (String) request.get("url"); - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = Collections.emptyMap(); - } - webView.loadUrl(url, headers); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void loadData(MethodCall methodCall, Result result) { - Log.d("FlutterWebView", "Call to load data" + methodCall); - - Map request = (Map) methodCall.arguments; - String baseUrl = (String) request.get("baseUrl"); - String data = (String) request.get("data"); - String mimeType = (String) request.get("mimeType"); - String encoding = (String) request.get("encoding"); - webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, ""); - result.success(null); - } - - private void setAcceptThirdPartyCookies(MethodCall methodCall, Result result) { - boolean accept = (boolean) methodCall.arguments; - CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.setAcceptThirdPartyCookies(webView, accept); - result.success(null); - } - - private void setCookie(MethodCall methodCall, Result result) { - Map args = (Map) methodCall.arguments; - String url = (String) args.get("url"); - String value = (String) args.get("value"); - CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.setCookie(url, value); - result.success(null); - } - - private void canGoBack(Result result) { - result.success(webView.canGoBack()); - } - - private void canGoForward(Result result) { - result.success(webView.canGoForward()); - } - - private void goBack(Result result) { - if (webView.canGoBack()) { - webView.goBack(); - } - result.success(null); - } - - private void goForward(Result result) { - if (webView.canGoForward()) { - webView.goForward(); - } - result.success(null); - } - - private void reload(Result result) { - webView.reload(); - result.success(null); - } - - private void currentUrl(Result result) { - result.success(webView.getUrl()); - } - - @SuppressWarnings("unchecked") - private void updateSettings(MethodCall methodCall, Result result) { - applySettings((Map) methodCall.arguments); - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { - String jsString = (String) methodCall.arguments; - if (jsString == null) { - throw new UnsupportedOperationException("JavaScript string cannot be null"); - } - webView.evaluateJavascript( - jsString, - new android.webkit.ValueCallback() { - @Override - public void onReceiveValue(String value) { - result.success(value); - } - }); - } - - @SuppressWarnings("unchecked") - private void addJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - registerJavaScriptChannelNames(channelNames); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void removeJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - for (String channelName : channelNames) { - webView.removeJavascriptInterface(channelName); - } - result.success(null); - } - - private void clearCache(Result result) { - webView.clearCache(true); - WebStorage.getInstance().deleteAllData(); - result.success(null); - } - - private void getTitle(Result result) { - result.success(webView.getTitle()); - } - - private void scrollTo(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollTo(x, y); - - result.success(null); - } - - private void scrollBy(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollBy(x, y); - result.success(null); - } - - private void getScrollX(Result result) { - result.success(webView.getScrollX()); - } - - private void getScrollY(Result result) { - result.success(webView.getScrollY()); - } - - private void applySettings(Map settings) { - for (String key : settings.keySet()) { - switch (key) { - case "jsMode": - Integer mode = (Integer) settings.get(key); - if (mode != null) updateJsMode(mode); - break; - case "hasNavigationDelegate": - final boolean hasNavigationDelegate = (boolean) settings.get(key); - - final WebViewClient webViewClient = - flutterWebViewClient.createWebViewClient(hasNavigationDelegate); - - webView.setWebViewClient(webViewClient); - break; - case "debuggingEnabled": - final boolean debuggingEnabled = (boolean) settings.get(key); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.setWebContentsDebuggingEnabled(debuggingEnabled); - } - break; - case "gestureNavigationEnabled": - break; - case "userAgent": - updateUserAgent((String) settings.get(key)); - break; - case "darkMode": - setDarkMode((boolean) settings.get(key)); - break; - default: - throw new IllegalArgumentException("Unknown WebView setting: " + key); - } - } - } - - private void setDarkMode(boolean darkMode) { - if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { - WebSettingsCompat.setAlgorithmicDarkeningAllowed(webView.getSettings(), darkMode); - } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { - int forceDarkMode = darkMode ? WebSettingsCompat.FORCE_DARK_ON : WebSettingsCompat.FORCE_DARK_OFF; - WebSettingsCompat.setForceDark(webView.getSettings(), forceDarkMode); - } else { - Log.d("FlutterWebView", "FORCE_DARK feature is not supported by this WebView"); - } - } - - private void updateJsMode(int mode) { - switch (mode) { - case 0: // disabled - webView.getSettings().setJavaScriptEnabled(false); - break; - case 1: // unrestricted - webView.getSettings().setJavaScriptEnabled(true); - break; - default: - throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); - } - } - - private void updateAutoMediaPlaybackPolicy(int mode) { - // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all - // other values we require a user gesture. - boolean requireUserGesture = mode != 1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); - } - } - - private void registerJavaScriptChannelNames(List channelNames) { - for (String channelName : channelNames) { - webView.addJavascriptInterface( - new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); - } - } - - private void updateUserAgent(String userAgent) { - webView.getSettings().setUserAgentString(userAgent); - } - - @Override - public void dispose() { - methodChannel.setMethodCallHandler(null); - webView.dispose(); - webView.destroy(); - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java deleted file mode 100644 index 24926bfc41..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; -import android.view.KeyEvent; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.RequiresApi; -import androidx.webkit.WebResourceErrorCompat; -import androidx.webkit.WebViewClientCompat; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -// We need to use WebViewClientCompat to get -// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) -// invoked by the webview on older Android devices, without it pages that use iframes will -// be broken when a navigationDelegate is set on Android version earlier than N. -class FlutterWebViewClient { - private static final String TAG = "FlutterWebViewClient"; - private final MethodChannel methodChannel; - private boolean hasNavigationDelegate; - - FlutterWebViewClient(MethodChannel methodChannel) { - this.methodChannel = methodChannel; - } - - private static String errorCodeToString(int errorCode) { - switch (errorCode) { - case WebViewClient.ERROR_AUTHENTICATION: - return "authentication"; - case WebViewClient.ERROR_BAD_URL: - return "badUrl"; - case WebViewClient.ERROR_CONNECT: - return "connect"; - case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: - return "failedSslHandshake"; - case WebViewClient.ERROR_FILE: - return "file"; - case WebViewClient.ERROR_FILE_NOT_FOUND: - return "fileNotFound"; - case WebViewClient.ERROR_HOST_LOOKUP: - return "hostLookup"; - case WebViewClient.ERROR_IO: - return "io"; - case WebViewClient.ERROR_PROXY_AUTHENTICATION: - return "proxyAuthentication"; - case WebViewClient.ERROR_REDIRECT_LOOP: - return "redirectLoop"; - case WebViewClient.ERROR_TIMEOUT: - return "timeout"; - case WebViewClient.ERROR_TOO_MANY_REQUESTS: - return "tooManyRequests"; - case WebViewClient.ERROR_UNKNOWN: - return "unknown"; - case WebViewClient.ERROR_UNSAFE_RESOURCE: - return "unsafeResource"; - case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: - return "unsupportedAuthScheme"; - case WebViewClient.ERROR_UNSUPPORTED_SCHEME: - return "unsupportedScheme"; - } - - final String message = - String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); - throw new IllegalArgumentException(message); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!hasNavigationDelegate) { - return false; - } - notifyOnNavigationRequest( - request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); - // We must make a synchronous decision here whether to allow the navigation or not, - // if the Dart code has set a navigation delegate we want that delegate to decide whether - // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we - // return true here to block the navigation, if the Dart delegate decides to allow the - // navigation the plugin will later make an addition loadUrl call for this url. - // - // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop - // navigations that target the main frame, if the request is not for the main frame - // we just return false to allow the navigation. - // - // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 - return request.isForMainFrame(); - } - - boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with - // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). - // On these devices we cannot tell whether the navigation is targeted to the main frame or not. - // We proceed assuming that the navigation is targeted to the main frame. If the page had any - // frames they will be loaded in the main frame instead. - Log.w( - TAG, - "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - private void onPageStarted(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageStarted", args); - } - - private void onPageFinished(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageFinished", args); - } - - private void onWebResourceError( - final int errorCode, final String description, final String failingUrl) { - final Map args = new HashMap<>(); - args.put("errorCode", errorCode); - args.put("description", description); - args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); - args.put("failingUrl", failingUrl); - methodChannel.invokeMethod("onWebResourceError", args); - } - - private void notifyOnNavigationRequest( - String url, Map headers, WebView webview, boolean isMainFrame) { - HashMap args = new HashMap<>(); - args.put("url", url); - args.put("isForMainFrame", isMainFrame); - if (isMainFrame) { - methodChannel.invokeMethod( - "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); - } else { - methodChannel.invokeMethod("navigationRequest", args); - } - } - - // This method attempts to avoid using WebViewClientCompat due to bug - // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see - // https://github.com/flutter/flutter/issues/29446. - WebViewClient createWebViewClient(boolean hasNavigationDelegate) { - this.hasNavigationDelegate = hasNavigationDelegate; - - if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return internalCreateWebViewClient(); - } - - return internalCreateWebViewClientCompat(); - } - - private WebViewClient internalCreateWebViewClient() { - return new WebViewClient() { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - @TargetApi(Build.VERSION_CODES.M) - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private WebViewClientCompat internalCreateWebViewClientCompat() { - return new WebViewClientCompat() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceErrorCompat error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private static class OnNavigationRequestResult implements MethodChannel.Result { - private final String url; - private final Map headers; - private final WebView webView; - - private OnNavigationRequestResult(String url, Map headers, WebView webView) { - this.url = url; - this.headers = headers; - this.webView = webView; - } - - @Override - public void success(Object shouldLoad) { - Boolean typedShouldLoad = (Boolean) shouldLoad; - if (typedShouldLoad) { - loadUrl(); - } - } - - @Override - public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigationRequest calls must succeed"); - } - - @Override - public void notImplemented() { - throw new IllegalStateException( - "navigationRequest must be implemented by the webview method channel"); - } - - private void loadUrl() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webView.loadUrl(url, headers); - } else { - webView.loadUrl(url); - } - } - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java deleted file mode 100644 index 9b81a5b7cc..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static android.content.Context.INPUT_METHOD_SERVICE; - -import android.content.Context; -import android.graphics.Rect; -import android.os.Build; -import android.util.Log; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebView; -import android.widget.ListPopupWindow; - -/** - * A WebView subclass that mirrors the same implementation hacks that the system WebView does in - * order to correctly create an InputConnection. - * - *

These hacks are only needed in Android versions below N and exist to create an InputConnection - * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in - * {@link #checkInputConnectionProxy}. - * - *

See also {@link ThreadedInputConnectionProxyAdapterView}. - */ -final class InputAwareWebView extends WebView { - private static final String TAG = "InputAwareWebView"; - private View threadedInputConnectionProxyView; - private ThreadedInputConnectionProxyAdapterView proxyAdapterView; - private View containerView; - - InputAwareWebView(Context context, View containerView) { - super(context); - this.containerView = containerView; - } - - void setContainerView(View containerView) { - this.containerView = containerView; - - if (proxyAdapterView == null) { - return; - } - - Log.w(TAG, "The containerView has changed while the proxyAdapterView exists."); - if (containerView != null) { - setInputConnectionTarget(proxyAdapterView); - } - } - - /** - * Set our proxy adapter view to use its cached input connection instead of creating new ones. - * - *

This is used to avoid losing our input connection when the virtual display is resized. - */ - void lockInputConnection() { - if (proxyAdapterView == null) { - return; - } - - proxyAdapterView.setLocked(true); - } - - /** Sets the proxy adapter view back to its default behavior. */ - void unlockInputConnection() { - if (proxyAdapterView == null) { - return; - } - - proxyAdapterView.setLocked(false); - } - - /** Restore the original InputConnection, if needed. */ - void dispose() { - resetInputConnection(); - } - - /** - * Creates an InputConnection from the IME thread when needed. - * - *

We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an - * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the - * system calling this method for WebView's proxy view in order to know when we need to create our - * own. - * - *

This method would normally be called for any View that used the InputMethodManager. We rely - * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the - * system WebView in order to know whether or not the system WebView expects an InputConnection on - * the IME thread. - */ - @Override - public boolean checkInputConnectionProxy(final View view) { - // Check to see if the view param is WebView's ThreadedInputConnectionProxyView. - View previousProxy = threadedInputConnectionProxyView; - threadedInputConnectionProxyView = view; - if (previousProxy == view) { - // This isn't a new ThreadedInputConnectionProxyView. Ignore it. - return super.checkInputConnectionProxy(view); - } - if (containerView == null) { - Log.e( - TAG, - "Can't create a proxy view because there's no container view. Text input may not work."); - return super.checkInputConnectionProxy(view); - } - - // We've never seen this before, so we make the assumption that this is WebView's - // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could - // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView. - proxyAdapterView = - new ThreadedInputConnectionProxyAdapterView( - /*containerView=*/ containerView, - /*targetView=*/ view, - /*imeHandler=*/ view.getHandler()); - setInputConnectionTarget(/*targetView=*/ proxyAdapterView); - return super.checkInputConnectionProxy(view); - } - - /** - * Ensure that input creation happens back on {@link #containerView}'s thread once this view no - * longer has focus. - * - *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's - * thread for all connections. We undo it here so users will be able to go back to typing in - * Flutter UIs as expected. - */ - @Override - public void clearFocus() { - super.clearFocus(); - resetInputConnection(); - } - - /** - * Ensure that input creation happens back on {@link #containerView}. - * - *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's - * thread for all connections. We undo it here so users will be able to go back to typing in - * Flutter UIs as expected. - */ - private void resetInputConnection() { - if (proxyAdapterView == null) { - // No need to reset the InputConnection to the default thread if we've never changed it. - return; - } - if (containerView == null) { - Log.e(TAG, "Can't reset the input connection to the container view because there is none."); - return; - } - setInputConnectionTarget(/*targetView=*/ containerView); - } - - /** - * This is the crucial trick that gets the InputConnection creation to happen on the correct - * thread pre Android N. - * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a - * - *

{@code targetView} should have a {@link View#getHandler} method with the thread that future - * InputConnections should be created on. - */ - private void setInputConnectionTarget(final View targetView) { - if (containerView == null) { - Log.e( - TAG, - "Can't set the input connection target because there is no containerView to use as a handler."); - return; - } - - targetView.requestFocus(); - containerView.post( - new Runnable() { - @Override - public void run() { - InputMethodManager imm = - (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); - // This is a hack to make InputMethodManager believe that the target view now has focus. - // As a result, InputMethodManager will think that targetView is focused, and will call - // getHandler() of the view when creating input connection. - - // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect - // the real window focus. - targetView.onWindowFocusChanged(true); - - // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call - // onCreateInputConnection() on targetView on the same thread as - // targetView.getHandler(). It will also call subsequent InputConnection methods on this - // thread. This is the IME thread in cases where targetView is our proxyAdapterView. - imm.isActive(containerView); - } - }); - } - - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - // This works around a crash when old (<67.0.3367.0) Chromium versions are used. - - // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown - // on tablets: - // - // - WebView is calling ListPopupWindow#show - // - buildDropDown is invoked, which sets mDropDownList to a DropDownListView. - // - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is - // also synchronously performing the following sequence: - // - WebView's focus change listener is loosing focus (as mDropDownList got it) - // - WebView is hiding all popups (as it lost focus) - // - WebView's SelectPopupDropDown#hide is invoked. - // - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null. - // - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null). - // - // To workaround this, we drop the problematic focus lost call. - // See more details on: https://github.com/flutter/flutter/issues/54164 - // - // We don't do this after Android P as it shipped with a new enough WebView version, and it's - // better to not do this on all future Android versions in case DropDownListView's code changes. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P - && isCalledFromListPopupWindowShow() - && !focused) { - return; - } - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - private boolean isCalledFromListPopupWindowShow() { - StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - for (StackTraceElement stackTraceElement : stackTraceElements) { - if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) - && stackTraceElement.getMethodName().equals("show")) { - return true; - } - } - return false; - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java deleted file mode 100644 index f23aae5b2b..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Handler; -import android.os.Looper; -import android.webkit.JavascriptInterface; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; - -/** - * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets - * up. - * - *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method - * channel to the Dart code. - */ -class JavaScriptChannel { - private final MethodChannel methodChannel; - private final String javaScriptChannelName; - private final Handler platformThreadHandler; - - /** - * @param methodChannel the Flutter WebView method channel to which JS messages are sent - * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method - * channel with each message to let the Dart code know which JavaScript channel the message - * was sent through - */ - JavaScriptChannel( - MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { - this.methodChannel = methodChannel; - this.javaScriptChannelName = javaScriptChannelName; - this.platformThreadHandler = platformThreadHandler; - } - - // Suppressing unused warning as this is invoked from JavaScript. - @SuppressWarnings("unused") - @JavascriptInterface - public void postMessage(final String message) { - Runnable postMessageRunnable = - new Runnable() { - @Override - public void run() { - HashMap arguments = new HashMap<>(); - arguments.put("channel", javaScriptChannelName); - arguments.put("message", message); - methodChannel.invokeMethod("javascriptChannelMessage", arguments); - } - }; - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); - } - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java deleted file mode 100644 index 8fbdfaff1a..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Handler; -import android.os.IBinder; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; - -/** - * A fake View only exposed to InputMethodManager. - * - *

This follows a similar flow to Chromium's WebView (see - * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionProxyView.java). - * WebView itself bounces its InputConnection around several different threads. We follow its logic - * here to get the same working connection. - * - *

This exists solely to forward input creation to WebView's ThreadedInputConnectionProxyView on - * the IME thread. The way that this is created in {@link - * InputAwareWebView#checkInputConnectionProxy} guarantees that we have a handle to - * ThreadedInputConnectionProxyView and {@link #onCreateInputConnection} is always called on the IME - * thread. We delegate to ThreadedInputConnectionProxyView there to get WebView's input connection. - */ -final class ThreadedInputConnectionProxyAdapterView extends View { - final Handler imeHandler; - final IBinder windowToken; - final View containerView; - final View rootView; - final View targetView; - - private boolean triggerDelayed = true; - private boolean isLocked = false; - private InputConnection cachedConnection; - - ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) { - super(containerView.getContext()); - this.imeHandler = imeHandler; - this.containerView = containerView; - this.targetView = targetView; - windowToken = containerView.getWindowToken(); - rootView = containerView.getRootView(); - setFocusable(true); - setFocusableInTouchMode(true); - setVisibility(VISIBLE); - } - - /** Returns whether or not this is currently asynchronously acquiring an input connection. */ - boolean isTriggerDelayed() { - return triggerDelayed; - } - - /** Sets whether or not this should use its previously cached input connection. */ - void setLocked(boolean locked) { - isLocked = locked; - } - - /** - * This is expected to be called on the IME thread. See the setup required for this in {@link - * InputAwareWebView#checkInputConnectionProxy(View)}. - * - *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. - */ - @Override - public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { - triggerDelayed = false; - InputConnection inputConnection = - (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); - triggerDelayed = true; - cachedConnection = inputConnection; - return inputConnection; - } - - @Override - public boolean checkInputConnectionProxy(View view) { - return true; - } - - @Override - public boolean hasWindowFocus() { - // None of our views here correctly report they have window focus because of how we're embedding - // the platform view inside of a virtual display. - return true; - } - - @Override - public View getRootView() { - return rootView; - } - - @Override - public boolean onCheckIsTextEditor() { - return true; - } - - @Override - public boolean isFocused() { - return true; - } - - @Override - public IBinder getWindowToken() { - return windowToken; - } - - @Override - public Handler getHandler() { - return imeHandler; - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java deleted file mode 100644 index 6fdc36fbe5..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.content.Context; -import android.view.View; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.StandardMessageCodec; -import io.flutter.plugin.platform.PlatformView; -import io.flutter.plugin.platform.PlatformViewFactory; -import java.util.Map; - -public final class WebViewFactory extends PlatformViewFactory { - private final BinaryMessenger messenger; - private final View containerView; - - WebViewFactory(BinaryMessenger messenger, View containerView) { - super(StandardMessageCodec.INSTANCE); - this.messenger = messenger; - this.containerView = containerView; - } - - @SuppressWarnings("unchecked") - @Override - public PlatformView create(Context context, int id, Object args) { - Map params = (Map) args; - return new FlutterWebView(context, messenger, id, params, containerView); - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java deleted file mode 100644 index 2de8fdf94b..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; - -/** - * Java platform implementation of the webview_flutter plugin. - * - *

Register this in an add to app scenario to gracefully handle activity and context changes. - * - *

Call {@link #registerWith(Registrar)} to use the stable {@code io.flutter.plugin.common} - * package instead. - */ -public class WebViewFlutterPlugin implements FlutterPlugin { - - private FlutterCookieManager flutterCookieManager; - - /** - * Add an instance of this to {@link io.flutter.embedding.engine.plugins.PluginRegistry} to - * register it. - * - *

THIS PLUGIN CODE PATH DEPENDS ON A NEWER VERSION OF FLUTTER THAN THE ONE DEFINED IN THE - * PUBSPEC.YAML. Text input will fail on some Android devices unless this is used with at least - * flutter/flutter@1d4d63ace1f801a022ea9ec737bf8c15395588b9. Use the V1 embedding with {@link - * #registerWith(Registrar)} to use this plugin with older Flutter versions. - * - *

Registration should eventually be handled automatically by v2 of the - * GeneratedPluginRegistrant. https://github.com/flutter/flutter/issues/42694 - */ - public WebViewFlutterPlugin() {} - - /** - * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common} - * package. - * - *

Calling this automatically initializes the plugin. However plugins initialized this way - * won't react to changes in activity or context, unlike {@link CameraPlugin}. - */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - registrar - .platformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new WebViewFactory(registrar.messenger(), registrar.view())); - new FlutterCookieManager(registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - BinaryMessenger messenger = binding.getBinaryMessenger(); - binding - .getPlatformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", new WebViewFactory(messenger, /*containerView=*/ null)); - flutterCookieManager = new FlutterCookieManager(messenger); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - if (flutterCookieManager == null) { - return; - } - - flutterCookieManager.dispose(); - flutterCookieManager = null; - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart b/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart deleted file mode 100644 index 2637e0d9f0..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; - -import 'webview_flutter.dart'; - -/// Interface for callbacks made by [WebViewPlatformController]. -/// -/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController]. -/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview. -abstract class WebViewPlatformCallbacksHandler { - /// Invoked by [WebViewPlatformController] when a JavaScript channel message is received. - void onJavaScriptChannelMessage(String channel, String message); - - /// Invoked by [WebViewPlatformController] when a navigation request is pending. - /// - /// If true is returned the navigation is allowed, otherwise it is blocked. - FutureOr onNavigationRequest({String url, bool isForMainFrame}); - - /// Invoked by [WebViewPlatformController] when a page has started loading. - void onPageStarted(String url); - - /// Invoked by [WebViewPlatformController] when a page has finished loading. - void onPageFinished(String url); - - /// Report web resource loading error to the host application. - void onWebResourceError(WebResourceError error); -} - -/// Possible error type categorizations used by [WebResourceError]. -enum WebResourceErrorType { - /// User authentication failed on server. - authentication, - - /// Malformed URL. - badUrl, - - /// Failed to connect to the server. - connect, - - /// Failed to perform SSL handshake. - failedSslHandshake, - - /// Generic file error. - file, - - /// File not found. - fileNotFound, - - /// Server or proxy hostname lookup failed. - hostLookup, - - /// Failed to read or write to the server. - io, - - /// User authentication failed on proxy. - proxyAuthentication, - - /// Too many redirects. - redirectLoop, - - /// Connection timed out. - timeout, - - /// Too many requests during this load. - tooManyRequests, - - /// Generic error. - unknown, - - /// Resource load was canceled by Safe Browsing. - unsafeResource, - - /// Unsupported authentication scheme (not basic or digest). - unsupportedAuthScheme, - - /// Unsupported URI scheme. - unsupportedScheme, - - /// The web content process was terminated. - webContentProcessTerminated, - - /// The web view was invalidated. - webViewInvalidated, - - /// A JavaScript exception occurred. - javaScriptExceptionOccurred, - - /// The result of JavaScript execution could not be returned. - javaScriptResultTypeIsUnsupported, -} - -/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. -class WebResourceError { - /// Creates a new [WebResourceError] - /// - /// A user should not need to instantiate this class, but will receive one in - /// [WebResourceErrorCallback]. - WebResourceError({ - required this.errorCode, - required this.description, - this.domain, - this.errorType, - this.failingUrl, - }) {} - - /// Raw code of the error from the respective platform. - /// - /// On Android, the error code will be a constant from a - /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and - /// will have a corresponding [errorType]. - /// - /// On iOS, the error code will be a constant from `NSError.code` in - /// Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. Some possible error codes - /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. - final int errorCode; - - /// The domain of where to find the error code. - /// - /// This field is only available on iOS and represents a "domain" from where - /// the [errorCode] is from. This value is taken directly from an `NSError` - /// in Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. - final String? domain; - - /// Description of the error that can be used to communicate the problem to the user. - final String description; - - /// The type this error can be categorized as. - /// - /// This will never be `null` on Android, but can be `null` on iOS. - final WebResourceErrorType? errorType; - - /// Gets the URL for which the resource request was made. - /// - /// This value is not provided on iOS. Alternatively, you can keep track of - /// the last values provided to [WebViewPlatformController.loadUrl]. - final String? failingUrl; -} - -/// Interface for talking to the webview's platform implementation. -/// -/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is -/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. -/// -/// Platform implementations that live in a separate package should extend this class rather than -/// implement it as webview_flutter does not consider newly added methods to be breaking changes. -/// Extending this class (using `extends`) ensures that the subclass will get the default -/// implementation, while platform implementations that `implements` this interface will be broken -/// by newly added [WebViewPlatformController] methods. -abstract class WebViewPlatformController { - /// Creates a new WebViewPlatform. - /// - /// Callbacks made by the WebView will be delegated to `handler`. - /// - /// The `handler` parameter must not be null. - WebViewPlatformController(WebViewPlatformCallbacksHandler handler); - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, - Map? headers, - ) { - throw UnimplementedError( - "WebView loadUrl is not implemented on the current platform"); - } - - Future loadData( - String? baseUrl, - String data, - String mimeType, - String encoding - ) { - throw UnimplementedError( - "WebView loadData is not implemented on the current platform"); - } - - Future setAcceptThirdPartyCookies(bool accept) { - throw UnimplementedError("WebView setAcceptThirdPartyCookies is not implemented on the current platform"); - } - - Future setCookie(String url, String value) { - throw UnimplementedError("WebView setCookie is not implemented on the current platform"); - } - - /// Updates the webview settings. - /// - /// Any non null field in `settings` will be set as the new setting value. - /// All null fields in `settings` are ignored. - Future updateSettings(WebSettings setting) { - throw UnimplementedError( - "WebView updateSettings is not implemented on the current platform"); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If no URL was ever loaded, returns `null`. - Future currentUrl() { - throw UnimplementedError( - "WebView currentUrl is not implemented on the current platform"); - } - - /// Checks whether there's a back history item. - Future canGoBack() { - throw UnimplementedError( - "WebView canGoBack is not implemented on the current platform"); - } - - /// Checks whether there's a forward history item. - Future canGoForward() { - throw UnimplementedError( - "WebView canGoForward is not implemented on the current platform"); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - throw UnimplementedError( - "WebView goBack is not implemented on the current platform"); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - throw UnimplementedError( - "WebView goForward is not implemented on the current platform"); - } - - /// Reloads the current URL. - Future reload() { - throw UnimplementedError( - "WebView reload is not implemented on the current platform"); - } - - /// Clears all caches used by the [WebView]. - /// - /// The following caches are cleared: - /// 1. Browser HTTP Cache. - /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - Future clearCache() { - throw UnimplementedError( - "WebView clearCache is not implemented on the current platform"); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// The Future completes with an error if a JavaScript error occurred, or if the type of the - /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). - Future evaluateJavascript(String javascriptString) { - throw UnimplementedError( - "WebView evaluateJavascript is not implemented on the current platform"); - } - - /// Adds new JavaScript channels to the set of enabled channels. - /// - /// For each value in this list the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - /// - /// See also: [CreationParams.javascriptChannelNames]. - Future addJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView addJavascriptChannels is not implemented on the current platform"); - } - - /// Removes JavaScript channel names from the set of enabled channels. - /// - /// This disables channels that were previously enabled by [addJavaScriptChannels] or through - /// [CreationParams.javascriptChannelNames]. - Future removeJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView removeJavascriptChannels is not implemented on the current platform"); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - throw UnimplementedError( - "WebView getTitle is not implemented on the current platform"); - } - - /// Set the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the position to scroll to in WebView pixels. - Future scrollTo(int x, int y) { - throw UnimplementedError( - "WebView scrollTo is not implemented on the current platform"); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by. - Future scrollBy(int x, int y) { - throw UnimplementedError( - "WebView scrollBy is not implemented on the current platform"); - } - - /// Return the horizontal scroll position of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - throw UnimplementedError( - "WebView getScrollX is not implemented on the current platform"); - } - - /// Return the vertical scroll position of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - throw UnimplementedError( - "WebView getScrollY is not implemented on the current platform"); - } -} - -/// A single setting for configuring a WebViewPlatform which may be absent. -class WebSetting { - /// Constructs an absent setting instance. - /// - /// The [isPresent] field for the instance will be false. - /// - /// Accessing [value] for an absent instance will throw. - WebSetting.absent() - : _value = null, - isPresent = false; - - /// Constructs a setting of the given `value`. - /// - /// The [isPresent] field for the instance will be true. - WebSetting.of(T value) - : _value = value, - isPresent = true; - - final T? _value; - - /// The setting's value. - /// - /// Throws if [WebSetting.isPresent] is false. - T get value { - if (!isPresent) { - throw StateError('Cannot access a value of an absent WebSetting'); - } - if (_value == null) { - throw StateError('Cannot access a value of an absent WebSetting'); - } - assert(isPresent); - return _value!; - } - - /// True when this web setting instance contains a value. - /// - /// When false the [WebSetting.value] getter throws. - final bool isPresent; - - @override - bool operator ==(other) { - if (other.runtimeType != runtimeType) return false; - if (other is! WebSetting) return false; - final WebSetting typedOther = other; - return typedOther.isPresent == isPresent && typedOther._value == _value; - } - - @override - int get hashCode => hashValues(_value, isPresent); -} - -/// Settings for configuring a WebViewPlatform. -/// -/// Initial settings are passed as part of [CreationParams], settings updates are sent with -/// [WebViewPlatform#updateSettings]. -/// -/// The `userAgent` parameter must not be null. -class WebSettings { - /// Construct an instance with initial settings. Future setting changes can be - /// sent with [WebviewPlatform#updateSettings]. - /// - /// The `userAgent` parameter must not be null. - WebSettings({ - this.javascriptMode, - this.hasNavigationDelegate, - this.darkMode, - this.debuggingEnabled, - this.gestureNavigationEnabled, - required this.userAgent, - }) {} - - /// The JavaScript execution mode to be used by the webview. - final JavascriptMode? javascriptMode; - - /// Whether the [WebView] has a [NavigationDelegate] set. - final bool? hasNavigationDelegate; - - final bool? darkMode; - - /// Whether to enable the platform's webview content debugging tools. - /// - /// See also: [WebView.debuggingEnabled]. - final bool? debuggingEnabled; - - /// The value used for the HTTP `User-Agent:` request header. - /// - /// If [userAgent.value] is null the platform's default user agent should be used. - /// - /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the - /// last time it was set. - /// - /// See also [WebView.userAgent]. - final WebSetting userAgent; - - /// Whether to allow swipe based navigation in iOS. - /// - /// See also: [WebView.gestureNavigationEnabled] - final bool? gestureNavigationEnabled; - - @override - String toString() { - return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent)'; - } -} - -/// Configuration to use when creating a new [WebViewPlatformController]. -/// -/// The `autoMediaPlaybackPolicy` parameter must not be null. -class CreationParams { - /// Constructs an instance to use when creating a new - /// [WebViewPlatformController]. - /// - /// The `autoMediaPlaybackPolicy` parameter must not be null. - CreationParams({ - this.initialUrl, - this.webSettings, - this.javascriptChannelNames, - this.userAgent, - this.autoMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) {} - - /// The initialUrl to load in the webview. - /// - /// When null the webview will be created without loading any page. - final String? initialUrl; - - /// The initial [WebSettings] for the new webview. - /// - /// This can later be updated with [WebViewPlatformController.updateSettings]. - final WebSettings? webSettings; - - /// The initial set of JavaScript channels that are configured for this webview. - /// - /// For each value in this set the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - // TODO(amirh): describe what should happen when postMessage is called once that code is migrated - // to PlatformWebView. - final Set? javascriptChannelNames; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - final String? userAgent; - - /// Which restrictions apply on automatic media playback. - final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; - - @override - String toString() { - return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; - } -} - -/// Signature for callbacks reporting that a [WebViewPlatformController] was created. -/// -/// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build]. -typedef WebViewPlatformCreatedCallback = void Function( - WebViewPlatformController webViewPlatformController); - -/// Interface for a platform implementation of a WebView. -/// -/// [WebView.platform] controls the builder that is used by [WebView]. -/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations -/// for Android and iOS respectively. -abstract class WebViewPlatform { - /// Builds a new WebView. - /// - /// Returns a Widget tree that embeds the created webview. - /// - /// `creationParams` are the initial parameters used to setup the webview. - /// - /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created - /// [WebViewPlatformController]. - /// - /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController] - /// implementation is created with the [WebViewPlatformController] instance as a parameter. - /// - /// `gestureRecognizers` specifies which gestures should be consumed by the web view. - /// It is possible for other gesture recognizers to be competing with the web view on pointer - /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle - /// vertical drags. The web view will claim gestures that are recognized by any of the - /// recognizers on this list. - /// When `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - /// - /// `webViewPlatformHandler` must not be null. - Widget build({ - BuildContext context, - // TODO(amirh): convert this to be the actual parameters. - // I'm starting without it as the PR is starting to become pretty big. - // I'll followup with the conversion PR. - CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set>? gestureRecognizers, - }); - - /// Clears all cookies for all [WebView] instances. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() { - throw UnimplementedError( - "WebView clearCookies is not implemented on the current platform"); - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart deleted file mode 100644 index f987f5b67f..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an Android webview. -/// -/// This is used as the default implementation for [WebView.platform] on Android. It uses -/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class AndroidWebView implements WebViewPlatform { - @override - Widget build({ - BuildContext? context, - CreationParams? creationParams, - @required WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - assert(webViewPlatformCallbacksHandler != null); - return GestureDetector( - // We prevent text selection by intercepting the long press event. - // This is a temporary stop gap due to issues with text selection on Android: - // https://github.com/flutter/flutter/issues/24585 - the text selection - // dialog is not responding to touch events. - // https://github.com/flutter/flutter/issues/24584 - the text selection - // handles are not showing. - // TODO(amirh): remove this when the issues above are fixed. - onLongPress: () {}, - excludeFromSemantics: true, - child: AndroidView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - // WebView content is not affected by the Android view's layout direction, - // we explicitly set it here so that the widget doesn't require an ambient - // directionality. - layoutDirection: TextDirection.rtl, - creationParams: - creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, - creationParamsCodec: const StandardMessageCodec(), - ), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart deleted file mode 100644 index 52e7b823fc..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an iOS webview. -/// -/// This is used as the default implementation for [WebView.platform] on iOS. It uses -/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class CupertinoWebView implements WebViewPlatform { - @override - Widget build({ - BuildContext? context, - CreationParams? creationParams, - @required WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - return UiKitView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - creationParams: - creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, - creationParamsCodec: const StandardMessageCodec(), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart deleted file mode 100644 index 7d7e232ff8..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; - -import '../platform_interface.dart'; - -/// A [WebViewPlatformController] that uses a method channel to control the webview. -class MethodChannelWebViewPlatform implements WebViewPlatformController { - /// Constructs an instance that will listen for webviews broadcasting to the - /// given [id], using the given [WebViewPlatformCallbacksHandler]. - MethodChannelWebViewPlatform(int id, this._platformCallbacksHandler) - : _channel = MethodChannel('plugins.flutter.io/webview_$id') { - _channel.setMethodCallHandler(_onMethodCall); - } - - final WebViewPlatformCallbacksHandler _platformCallbacksHandler; - - final MethodChannel _channel; - - static const MethodChannel _cookieManagerChannel = - MethodChannel('plugins.flutter.io/cookie_manager'); - - Future _onMethodCall(MethodCall call) async { - switch (call.method) { - case 'javascriptChannelMessage': - final String channel = call.arguments['channel']; - final String message = call.arguments['message']; - _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); - return true; - case 'navigationRequest': - return await _platformCallbacksHandler.onNavigationRequest( - url: call.arguments['url'], - isForMainFrame: call.arguments['isForMainFrame'], - ); - case 'onPageFinished': - _platformCallbacksHandler.onPageFinished(call.arguments['url']); - return null; - case 'onPageStarted': - _platformCallbacksHandler.onPageStarted(call.arguments['url']); - return null; - case 'onWebResourceError': - _platformCallbacksHandler.onWebResourceError( - WebResourceError( - errorCode: call.arguments['errorCode'], - description: call.arguments['description'], - domain: call.arguments['domain'], - failingUrl: call.arguments['failingUrl'], - errorType: call.arguments['errorType'] == null - ? WebResourceErrorType.unknown - : WebResourceErrorType.values.firstWhere( - (WebResourceErrorType type) { - return type.toString() == - '$WebResourceErrorType.${call.arguments['errorType']}'; - }, - ), - ), - ); - return null; - } - - throw MissingPluginException( - '${call.method} was invoked but has no handler', - ); - } - - @override - Future loadUrl( - String url, - Map? headers, - ) async { - return _channel.invokeMethod('loadUrl', { - 'url': url, - 'headers': headers, - }); - } - - @override - Future loadData(String? baseUrl, String data, String mimeType, String encoding) async { - return _channel.invokeMethod('loadData', { - 'baseUrl': baseUrl, - 'data': data, - 'mimeType': mimeType, - 'encoding': encoding, - }); - } - - @override - Future setAcceptThirdPartyCookies(bool accept) { - return _channel.invokeMethod('setAcceptThirdPartyCookies', accept); - } - - @override - Future setCookie(String url, String value) { - return _channel.invokeMethod( - 'setCookie', - { - 'url': url, - 'value': value, - }, - ); - } - - @override - Future currentUrl() => _channel.invokeMethod('currentUrl'); - - @override - Future canGoBack() => _channel.invokeMethod("canGoBack"); - - @override - Future canGoForward() => _channel.invokeMethod("canGoForward"); - - @override - Future goBack() => _channel.invokeMethod("goBack"); - - @override - Future goForward() => _channel.invokeMethod("goForward"); - - @override - Future reload() => _channel.invokeMethod("reload"); - - @override - Future clearCache() => _channel.invokeMethod("clearCache"); - - @override - Future updateSettings(WebSettings settings) { - final Map updatesMap = _webSettingsToMap(settings); - if (updatesMap.isEmpty) { - return Future.value(); - } - return _channel.invokeMethod('updateSettings', updatesMap); - } - - @override - Future evaluateJavascript(String javascriptString) { - return _channel.invokeMethod('evaluateJavascript', javascriptString); - } - - @override - Future addJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'addJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future removeJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'removeJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future getTitle() => _channel.invokeMethod("getTitle"); - - @override - Future scrollTo(int x, int y) { - return _channel.invokeMethod('scrollTo', { - 'x': x, - 'y': y, - }); - } - - @override - Future scrollBy(int x, int y) { - return _channel.invokeMethod('scrollBy', { - 'x': x, - 'y': y, - }); - } - - @override - Future getScrollX() => _channel.invokeMethod("getScrollX"); - - @override - Future getScrollY() => _channel.invokeMethod("getScrollY"); - - /// Method channel implementation for [WebViewPlatform.clearCookies]. - static Future clearCookies() { - return _cookieManagerChannel - .invokeMethod('clearCookies') - .then((dynamic result) => result); - } - - static Map _webSettingsToMap(WebSettings? settings) { - final Map map = {}; - void _addIfNonNull(String key, dynamic value) { - if (value == null) { - return; - } - map[key] = value; - } - - void _addSettingIfPresent(String key, WebSetting setting) { - if (!setting.isPresent) { - return; - } - map[key] = setting.value; - } - - _addIfNonNull('jsMode', settings?.javascriptMode?.index); - _addIfNonNull('hasNavigationDelegate', settings?.hasNavigationDelegate); - _addIfNonNull('darkMode', settings?.darkMode); - _addIfNonNull('debuggingEnabled', settings?.debuggingEnabled); - _addIfNonNull( - 'gestureNavigationEnabled', settings?.gestureNavigationEnabled); - _addSettingIfPresent('userAgent', settings?.userAgent as WebSetting); - return map; - } - - /// Converts a [CreationParams] object to a map as expected by `platform_views` channel. - /// - /// This is used for the `creationParams` argument of the platform views created by - /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. - static Map creationParamsToMap( - CreationParams creationParams) { - return { - 'initialUrl': creationParams.initialUrl, - 'settings': _webSettingsToMap(creationParams.webSettings), - 'javascriptChannelNames': creationParams.javascriptChannelNames?.toList(), - 'userAgent': creationParams.userAgent, - 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, - }; - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart b/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart deleted file mode 100644 index 3a846f6c22..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart +++ /dev/null @@ -1,823 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import 'platform_interface.dart'; -import 'src/webview_android.dart'; -import 'src/webview_cupertino.dart'; -import 'src/webview_method_channel.dart'; - -/// Optional callback invoked when a web view is first created. [controller] is -/// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); - -/// Describes the state of JavaScript support in a given web view. -enum JavascriptMode { - /// JavaScript execution is disabled. - disabled, - - /// JavaScript execution is not restricted. - unrestricted, -} - -/// A message that was sent by JavaScript code running in a [WebView]. -class JavascriptMessage { - /// Constructs a JavaScript message object. - /// - /// The `message` parameter must not be null. - const JavascriptMessage(this.message); - - /// The contents of the message that was sent by the JavaScript code. - final String message; -} - -/// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); - -/// Information about a navigation action that is about to be executed. -class NavigationRequest { - NavigationRequest._({ required this.url, required this.isForMainFrame}); - - /// The URL that will be loaded if the navigation is executed. - final String url; - - /// Whether the navigation request is to be loaded as the main frame. - final bool isForMainFrame; - - @override - String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; - } -} - -/// A decision on how to handle a navigation request. -enum NavigationDecision { - /// Prevent the navigation from taking place. - prevent, - - /// Allow the navigation to take place. - navigate, -} - -/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget. -/// -/// To use this, set [WebView.platform] to an instance of this class. -/// -/// This implementation uses hybrid composition to render the [WebView] on -/// Android. It solves multiple issues related to accessibility and interaction -/// with the [WebView] at the cost of some performance on Android versions below -/// 10. See https://github.com/flutter/flutter/wiki/Hybrid-Composition for more -/// information. -class SurfaceAndroidWebView extends AndroidWebView { - @override - Widget build({ - BuildContext? context, - CreationParams? creationParams, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, - }) { - assert(webViewPlatformCallbacksHandler != null); - return PlatformViewLink( - viewType: 'plugins.flutter.io/webview', - surfaceFactory: ( - BuildContext context, - PlatformViewController controller, - ) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, - viewType: 'plugins.flutter.io/webview', - // WebView content is not affected by the Android view's layout direction, - // we explicitly set it here so that the widget doesn't require an ambient - // directionality. - layoutDirection: TextDirection.rtl, - creationParams: creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, - creationParamsCodec: const StandardMessageCodec(), - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..addOnPlatformViewCreatedListener((int id) { - if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler), - ); - }) - ..create(); - }, - ); - } -} - -/// Decides how to handle a specific navigation request. -/// -/// The returned [NavigationDecision] determines how the navigation described by -/// `navigation` should be handled. -/// -/// See also: [WebView.navigationDelegate]. -typedef FutureOr NavigationDelegate( - NavigationRequest navigation); - -/// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); - -/// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); - -/// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); - -/// Specifies possible restrictions on automatic media playback. -/// -/// This is typically used in [WebView.initialMediaPlaybackPolicy]. -// The method channel implementation is marshalling this enum to the value's index, so the order -// is important. -enum AutoMediaPlaybackPolicy { - /// Starting any kind of media playback requires a user action. - /// - /// For example: JavaScript code cannot start playing media unless the code was executed - /// as a result of a user action (like a touch event). - require_user_action_for_all_media_types, - - /// Starting any kind of media playback is always allowed. - /// - /// For example: JavaScript code that's triggered when the page is loaded can start playing - /// video or audio. - always_allow, -} - -final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); - -/// A named channel for receiving messaged from JavaScript code running inside a web view. -class JavascriptChannel { - /// Constructs a Javascript channel. - /// - /// The parameters `name` and `onMessageReceived` must not be null. - JavascriptChannel({ - required this.name, - required this.onMessageReceived, - }) : assert(_validChannelNames.hasMatch(name)); - - /// The channel's name. - /// - /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to - /// the Javascript window object's property named `name`. - /// - /// The name must start with a letter or underscore(_), followed by any combination of those - /// characters plus digits. - /// - /// Note that any JavaScript existing `window` property with this name will be overriden. - /// - /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. - final String name; - - /// A callback that's invoked when a message is received through the channel. - final JavascriptMessageHandler onMessageReceived; -} - -/// A web view widget for showing html content. -/// -/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering -/// the `WebView` is not able to block the `WebView` from receiving touch events. -/// See https://github.com/flutter/flutter/issues/53490. -class WebView extends StatefulWidget { - /// Creates a new web view. - /// - /// The web view can be controlled using a `WebViewController` that is passed to the - /// `onWebViewCreated` callback once the web view is created. - /// - /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. - const WebView({ - Key? key, - this.onWebViewCreated, - this.initialUrl, - this.javascriptMode = JavascriptMode.disabled, - this.javascriptChannels, - this.navigationDelegate, - this.gestureRecognizers, - this.onPageStarted, - this.onPageFinished, - this.onWebResourceError, - this.darkMode = false, - this.debuggingEnabled = false, - this.gestureNavigationEnabled = false, - this.userAgent, - this.initialMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) : super(key: key); - - static WebViewPlatform? _platform; - - /// Sets a custom [WebViewPlatform]. - /// - /// This property can be set to use a custom platform implementation for WebViews. - /// - /// Setting `platform` doesn't affect [WebView]s that were already created. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static set platform(WebViewPlatform platform) { - _platform = platform; - } - - /// The WebView platform that's used by this WebView. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static WebViewPlatform get platform { - if (_platform == null) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - _platform = AndroidWebView(); - break; - case TargetPlatform.iOS: - _platform = CupertinoWebView(); - break; - default: - throw UnsupportedError( - "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); - } - } - return _platform!; - } - - /// If not null invoked once the web view is created. - final WebViewCreatedCallback? onWebViewCreated; - - /// Which gestures should be consumed by the web view. - /// - /// It is possible for other gesture recognizers to be competing with the web view on pointer - /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle - /// vertical drags. The web view will claim gestures that are recognized by any of the - /// recognizers on this list. - /// - /// When this set is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - final Set>? gestureRecognizers; - - /// The initial URL to load. - final String? initialUrl; - - /// Whether Javascript execution is enabled. - final JavascriptMode javascriptMode; - - /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. - /// - /// For each [JavascriptChannel] in the set, a channel object is made available for the - /// JavaScript code in a window property named [JavascriptChannel.name]. - /// The JavaScript code can then call `postMessage` on that object to send a message that will be - /// passed to [JavascriptChannel.onMessageReceived]. - /// - /// For example for the following JavascriptChannel: - /// - /// ```dart - /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); - /// ``` - /// - /// JavaScript code can call: - /// - /// ```javascript - /// Print.postMessage('Hello'); - /// ``` - /// - /// To asynchronously invoke the message handler which will print the message to standard output. - /// - /// Adding a new JavaScript channel only takes affect after the next page is loaded. - /// - /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple - /// channels in the list. - /// - /// A null value is equivalent to an empty set. - final Set? javascriptChannels; - - /// A delegate function that decides how to handle navigation actions. - /// - /// When a navigation is initiated by the WebView (e.g when a user clicks a link) - /// this delegate is called and has to decide how to proceed with the navigation. - /// - /// See [NavigationDecision] for possible decisions the delegate can take. - /// - /// When null all navigation actions are allowed. - /// - /// Caveats on Android: - /// - /// * Navigation actions targeted to the main frame can be intercepted, - /// navigation actions targeted to subframes are allowed regardless of the value - /// returned by this delegate. - /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were - /// triggered by a user gesture, this disables some of Chromium's security mechanisms. - /// A navigationDelegate should only be set when loading trusted content. - /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have - /// a later version): - /// * When a navigationDelegate is set pages with frames are not properly handled by the - /// webview, and frames will be opened in the main frame. - /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. - final NavigationDelegate? navigationDelegate; - - /// Invoked when a page starts loading. - final PageStartedCallback? onPageStarted; - - /// Invoked when a page has finished loading. - /// - /// This is invoked only for the main frame. - /// - /// When [onPageFinished] is invoked on Android, the page being rendered may - /// not be updated yet. - /// - /// When invoked on iOS or Android, any Javascript code that is embedded - /// directly in the HTML has been loaded and code injected with - /// [WebViewController.evaluateJavascript] can assume this. - final PageFinishedCallback? onPageFinished; - - /// Invoked when a web resource has failed to load. - /// - /// This can be called for any resource (iframe, image, etc.), not just for - /// the main page. - final WebResourceErrorCallback? onWebResourceError; - - final bool? darkMode; - - /// Controls whether WebView debugging is enabled. - /// - /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). - /// - /// WebView debugging is enabled by default in dev builds on iOS. - /// - /// To debug WebViews on iOS: - /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) - /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> - /// - /// By default `debuggingEnabled` is false. - final bool debuggingEnabled; - - /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. - /// - /// This only works on iOS. - /// - /// By default `gestureNavigationEnabled` is false. - final bool gestureNavigationEnabled; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - /// - /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. - /// - /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. - /// - /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom - /// user agent. - /// - /// By default `userAgent` is null. - final String? userAgent; - - /// Which restrictions apply on automatic media playback. - /// - /// This initial value is applied to the platform's webview upon creation. Any following - /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). - /// - /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. - final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; - - @override - State createState() => _WebViewState(); -} - -class _WebViewState extends State { - final Completer _controller = - Completer(); - - late _PlatformCallbacksHandler _platformCallbacksHandler; - - @override - Widget build(BuildContext context) { - return WebView.platform.build( - context: context, - onWebViewPlatformCreated: _onWebViewPlatformCreated, - webViewPlatformCallbacksHandler: _platformCallbacksHandler, - gestureRecognizers: widget.gestureRecognizers, - creationParams: _creationParamsfromWidget(widget), - ); - } - - @override - void initState() { - super.initState(); - _assertJavascriptChannelNamesAreUnique(); - _platformCallbacksHandler = _PlatformCallbacksHandler(widget); - } - - @override - void didUpdateWidget(WebView oldWidget) { - super.didUpdateWidget(oldWidget); - _assertJavascriptChannelNamesAreUnique(); - _controller.future.then((WebViewController controller) { - _platformCallbacksHandler._widget = widget; - controller._updateWidget(widget); - }); - } - - void _onWebViewPlatformCreated(WebViewPlatformController webViewPlatform) { - final WebViewController controller = - WebViewController._(widget, webViewPlatform, _platformCallbacksHandler); - _controller.complete(controller); - if (widget.onWebViewCreated != null) { - widget.onWebViewCreated!(controller); - } - } - - void _assertJavascriptChannelNamesAreUnique() { - if (widget.javascriptChannels == null || - widget.javascriptChannels!.isEmpty) { - return; - } - assert(_extractChannelNames(widget.javascriptChannels)?.length == - widget.javascriptChannels!.length); - } -} - -CreationParams _creationParamsfromWidget(WebView widget) { - return CreationParams( - initialUrl: widget.initialUrl, - webSettings: _webSettingsFromWidget(widget), - javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), - userAgent: widget.userAgent, - autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, - ); -} - -WebSettings _webSettingsFromWidget(WebView widget) { - return WebSettings( - javascriptMode: widget.javascriptMode, - hasNavigationDelegate: widget.navigationDelegate != null, - darkMode: widget.darkMode, - debuggingEnabled: widget.debuggingEnabled, - gestureNavigationEnabled: widget.gestureNavigationEnabled, - userAgent: WebSetting.of(widget.userAgent ?? ''), - ); -} - -// This method assumes that no fields in `currentValue` are null. -WebSettings _clearUnchangedWebSettings( - WebSettings currentValue, WebSettings newValue) { - assert(currentValue.javascriptMode != null); - assert(currentValue.hasNavigationDelegate != null); - assert(currentValue.darkMode != null); - assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent.isPresent); - assert(newValue.javascriptMode != null); - assert(newValue.hasNavigationDelegate != null); - assert(newValue.darkMode != null); - assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent.isPresent); - - JavascriptMode? javascriptMode; - bool? hasNavigationDelegate; - bool? darkMode; - bool? debuggingEnabled; - WebSetting userAgent = WebSetting.absent(); - if (currentValue.javascriptMode != newValue.javascriptMode) { - javascriptMode = newValue.javascriptMode; - } - if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { - hasNavigationDelegate = newValue.hasNavigationDelegate; - } - if (currentValue.darkMode != newValue.darkMode) { - darkMode = newValue.darkMode; - } - if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { - debuggingEnabled = newValue.debuggingEnabled; - } - if (currentValue.userAgent != newValue.userAgent) { - userAgent = newValue.userAgent; - } - - return WebSettings( - javascriptMode: javascriptMode, - hasNavigationDelegate: hasNavigationDelegate, - darkMode: darkMode, - debuggingEnabled: debuggingEnabled, - userAgent: userAgent, - ); -} - -Set? _extractChannelNames(Set? channels) { - final Set channelNames = channels == null - ? {} - : channels.map((JavascriptChannel channel) => channel.name).toSet(); - return channelNames; -} - -class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { - _PlatformCallbacksHandler(this._widget) { - _updateJavascriptChannelsFromSet(_widget.javascriptChannels); - } - - WebView _widget; - - // Maps a channel name to a channel. - final Map _javascriptChannels = - {}; - - @override - void onJavaScriptChannelMessage(String channel, String message) { - _javascriptChannels[channel]?.onMessageReceived(JavascriptMessage(message)); - } - - @override - FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) async { - final NavigationRequest request = - NavigationRequest._(url: url!, isForMainFrame: isForMainFrame!); - final bool allowNavigation = _widget.navigationDelegate == null || - await _widget.navigationDelegate!(request) == - NavigationDecision.navigate; - return allowNavigation; - } - - @override - void onPageStarted(String url) { - if (_widget.onPageStarted != null) { - _widget.onPageStarted!(url); - } - } - - @override - void onPageFinished(String url) { - if (_widget.onPageFinished != null) { - _widget.onPageFinished!(url); - } - } - - @override - void onWebResourceError(WebResourceError error) { - if (_widget.onWebResourceError != null) { - _widget.onWebResourceError!(error); - } - } - - void _updateJavascriptChannelsFromSet(Set? channels) { - _javascriptChannels.clear(); - if (channels == null) { - return; - } - for (JavascriptChannel channel in channels) { - _javascriptChannels[channel.name] = channel; - } - } -} - -/// Controls a [WebView]. -/// -/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] -/// callback for a [WebView] widget. -class WebViewController { - WebViewController._( - this._widget, - this._webViewPlatformController, - this._platformCallbacksHandler, - ) { - _settings = _webSettingsFromWidget(_widget); - } - - final WebViewPlatformController _webViewPlatformController; - - final _PlatformCallbacksHandler _platformCallbacksHandler; - - late WebSettings _settings; - - WebView _widget; - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, { - Map? headers, - }) async { - _validateUrlString(url); - return _webViewPlatformController.loadUrl(url, headers); - } - - Future loadData( - String? baseUrl, - String data, - String mimeType, - String encoding) async { - return _webViewPlatformController.loadData(baseUrl, data, mimeType, encoding); - } - - Future setAcceptThirdPartyCookies(bool accept) async { - return _webViewPlatformController.setAcceptThirdPartyCookies(accept); - } - - Future setCookie(String url, String value) async { - return _webViewPlatformController.setCookie(url, value); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If [WebView.initialUrl] was never specified, returns `null`. - /// Note that this operation is asynchronous, and it is possible that the - /// current URL changes again by the time this function returns (in other - /// words, by the time this future completes, the WebView may be displaying a - /// different URL). - Future currentUrl() { - return _webViewPlatformController.currentUrl(); - } - - /// Checks whether there's a back history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has - /// changed by the time the future completed. - Future canGoBack() { - return _webViewPlatformController.canGoBack(); - } - - /// Checks whether there's a forward history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has - /// changed by the time the future completed. - Future canGoForward() { - return _webViewPlatformController.canGoForward(); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - return _webViewPlatformController.goBack(); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - return _webViewPlatformController.goForward(); - } - - /// Reloads the current URL. - Future reload() { - return _webViewPlatformController.reload(); - } - - /// Clears all caches used by the [WebView]. - /// - /// The following caches are cleared: - /// 1. Browser HTTP Cache. - /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - /// - /// Note: Calling this method also triggers a reload. - Future clearCache() async { - await _webViewPlatformController.clearCache(); - return reload(); - } - - Future _updateWidget(WebView widget) async { - _widget = widget; - await _updateSettings(_webSettingsFromWidget(widget)); - await _updateJavascriptChannels(widget.javascriptChannels); - } - - Future _updateSettings(WebSettings newSettings) { - final WebSettings update = - _clearUnchangedWebSettings(_settings, newSettings); - _settings = newSettings; - return _webViewPlatformController.updateSettings(update); - } - - Future _updateJavascriptChannels( - Set? newChannels) async { - final Set currentChannels = - _platformCallbacksHandler._javascriptChannels.keys.toSet(); - final Set newChannelNames = _extractChannelNames(newChannels)!; - final Set channelsToAdd = - newChannelNames.difference(currentChannels); - final Set channelsToRemove = - currentChannels.difference(newChannelNames); - if (channelsToRemove.isNotEmpty) { - await _webViewPlatformController - .removeJavascriptChannels(channelsToRemove); - } - if (channelsToAdd.isNotEmpty) { - await _webViewPlatformController.addJavascriptChannels(channelsToAdd); - } - _platformCallbacksHandler._updateJavascriptChannelsFromSet(newChannels); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. - /// - /// On iOS depending on the value type the return value would be one of: - /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. - /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. - /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript - /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String? javascriptString) { - if (_settings.javascriptMode == JavascriptMode.disabled) { - return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); - } - if (javascriptString == null) { - return Future.error( - ArgumentError('The argument javascriptString must not be null.')); - } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - return _webViewPlatformController.getTitle(); - } - - /// Sets the WebView's content scroll position. - /// - /// The parameters `x` and `y` specify the scroll position in WebView pixels. - Future scrollTo(int x, int y) { - return _webViewPlatformController.scrollTo(x, y); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. - Future scrollBy(int x, int y) { - return _webViewPlatformController.scrollBy(x, y); - } - - /// Return the horizontal scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - return _webViewPlatformController.getScrollX(); - } - - /// Return the vertical scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - return _webViewPlatformController.getScrollY(); - } -} - -/// Manages cookies pertaining to all [WebView]s. -class CookieManager { - /// Creates a [CookieManager] -- returns the instance if it's already been called. - factory CookieManager() { - return _instance ??= CookieManager._(); - } - - CookieManager._(); - - static CookieManager? _instance; - - /// Clears all cookies for all [WebView] instances. - /// - /// This is a no op on iOS version smaller than 9. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() => WebView.platform.clearCookies(); -} - -// Throws an ArgumentError if `url` is not a valid URL string. -void _validateUrlString(String url) { - try { - final Uri uri = Uri.parse(url); - if (uri.scheme.isEmpty) { - throw ArgumentError('Missing scheme in URL string: "$url"'); - } - } on FormatException catch (e) { - throw ArgumentError(e); - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/pubspec.lock b/apps/flutter_parent/plugins/webview_flutter/pubspec.lock deleted file mode 100644 index e30d186c42..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/pubspec.lock +++ /dev/null @@ -1,220 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.2" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.2" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_driver: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - fuchsia_remote_debug_protocol: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0+1" - platform: - dependency: transitive - description: - name: platform - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - process: - dependency: transitive - description: - name: process - url: "https://pub.dartlang.org" - source: hosted - version: "4.2.3" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - sync_http: - dependency: transitive - description: - name: sync_http - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.2" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - vm_service: - dependency: transitive - description: - name: vm_service - url: "https://pub.dartlang.org" - source: hosted - version: "7.1.1" - webdriver: - dependency: transitive - description: - name: webdriver - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" -sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=2.5.3" diff --git a/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml b/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml deleted file mode 100644 index 60fc18d241..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: webview_flutter -description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 1.0.7 -homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter -publish_to: none - -environment: - sdk: ">=3.0.0 <3.10.6" - flutter: 3.13.2 - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - pedantic: ^1.8.0 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.webviewflutter - pluginClass: WebViewFlutterPlugin - ios: - pluginClass: FLTWebViewFlutterPlugin diff --git a/apps/flutter_parent/pubspec.lock b/apps/flutter_parent/pubspec.lock index ee48ed2d57..f05ba9414b 100644 --- a/apps/flutter_parent/pubspec.lock +++ b/apps/flutter_parent/pubspec.lock @@ -542,10 +542,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975" + sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3" url: "https://pub.dev" source: hosted - version: "15.1.0+1" + version: "16.1.0" flutter_local_notifications_linux: dependency: transitive description: @@ -1518,10 +1518,35 @@ packages: webview_flutter: dependency: "direct main" description: - path: "plugins/webview_flutter" - relative: true - source: path - version: "1.0.7" + name: webview_flutter + sha256: "392c1d83b70fe2495de3ea2c84531268d5b8de2de3f01086a53334d8b6030a88" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" + url: "https://pub.dev" + source: hosted + version: "2.10.4" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" + url: "https://pub.dev" + source: hosted + version: "1.9.5" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 + url: "https://pub.dev" + source: hosted + version: "2.9.5" win32: dependency: transitive description: diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml index 66d9770a5f..013bf5be43 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.9.0+48 +version: 3.9.1+49 module: androidX: true @@ -85,13 +85,11 @@ dependencies: device_info_plus: ^9.0.2 encrypted_shared_preferences: # Used by ApiPrefs to securely store data path: ./plugins/encrypted_shared_preferences - flutter_local_notifications: ^15.1.0+1 + flutter_local_notifications: ^16.1.0 package_info_plus: ^4.0.2 permission_handler: ^10.4.3 shared_preferences: ^2.2.0 # Used to cache remote config properties - #webview_flutter: 0.3.19+5 - webview_flutter: # TODO: Remove once the flutter plugin supports baseUrl https://github.com/flutter/plugins/pull/2463 - path: ./plugins/webview_flutter + webview_flutter: ^3.0.4 # Routing fluro: ^2.0.5 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 89ff60074a..ac2f283561 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 @@ -38,10 +38,12 @@ 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_colors.dart'; import 'package:flutter_parent/utils/design/student_color_set.dart'; +import 'package:flutter_parent/utils/permission_handler.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:permission_handler/permission_handler.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; @@ -59,6 +61,7 @@ void main() { final interactor = MockAssignmentDetailsInteractor(); final convoInteractor = MockCreateConversationInteractor(); + final permissionHandler = MockPermissionHandler(); final student = User((b) => b ..id = studentId @@ -85,6 +88,7 @@ void main() { locator.registerFactory(() => convoInteractor); locator.registerFactory(() => WebContentInteractor()); locator.registerFactory(() => QuickNav()); + locator.registerFactory(() => permissionHandler); }); setUp(() { @@ -506,6 +510,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('creates reminder without due date', (tester) async { + when(permissionHandler.checkPermissionStatus(Permission.scheduleExactAlarm)).thenAnswer((realInvocation) => Future.value(PermissionStatus.granted)); when(interactor.loadAssignmentDetails(any, courseId, assignmentId, studentId)) .thenAnswer((_) async => AssignmentDetails(assignment: assignment)); @@ -546,6 +551,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('creates reminder with due date', (tester) async { + when(permissionHandler.checkPermissionStatus(Permission.scheduleExactAlarm)).thenAnswer((realInvocation) => Future.value(PermissionStatus.granted)); final date = DateTime.now().add(Duration(hours: 1)); when(interactor.loadAssignmentDetails(any, any, any, any)) .thenAnswer((_) async => AssignmentDetails(assignment: assignment.rebuild((b) => b..dueAt = date))); diff --git a/apps/flutter_parent/test/utils/notification_util_test.dart b/apps/flutter_parent/test/utils/notification_util_test.dart index 516e05cde3..65d5fd0495 100644 --- a/apps/flutter_parent/test/utils/notification_util_test.dart +++ b/apps/flutter_parent/test/utils/notification_util_test.dart @@ -25,15 +25,14 @@ import 'package:flutter_parent/utils/db/reminder_db.dart'; import 'package:flutter_parent/utils/notification_util.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; import 'test_app.dart'; import 'test_helpers/mock_helpers.mocks.dart'; -import 'package:timezone/data/latest_all.dart' as tz; -import 'package:timezone/timezone.dart' as tz; - void main() { - final plugin = MockFlutterLocalNotificationsPlugin(); + final plugin = MockAndroidFlutterLocalNotificationsPlugin(); final database = MockReminderDb(); final analytics = MockAnalytics(); @@ -57,9 +56,8 @@ void main() { onDidReceiveNotificationResponse: captureAnyNamed('onDidReceiveNotificationResponse'), )); - InitializationSettings initSettings = verification.captured[0]; - expect(initSettings.android?.defaultIcon, 'ic_notification_canvas_logo'); - expect(initSettings.iOS, null); + AndroidInitializationSettings initSettings = verification.captured[0]; + expect(initSettings.defaultIcon, 'ic_notification_canvas_logo'); var callback = verification.captured[1]; expect(callback, isNotNull); @@ -141,20 +139,19 @@ void main() { var d = reminder.date!.toUtc(); var date = tz.TZDateTime.utc(d.year, d.month, d.day, d.hour, d.minute, d.second); - final NotificationDetails details = verify(plugin.zonedSchedule( + final AndroidNotificationDetails details = verify(plugin.zonedSchedule( reminder.id, 'title', 'body', date, captureAny, - payload: json.encode(serialize(expectedPayload)), - uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, + scheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + payload: json.encode(serialize(expectedPayload)) )).captured.first; - expect(details.iOS, isNull); - expect(details.android?.channelId, NotificationUtil.notificationChannelReminders); - expect(details.android?.channelName, AppLocalizations().remindersNotificationChannelName); - expect(details.android?.channelDescription, AppLocalizations().remindersNotificationChannelDescription); + expect(details.channelId, NotificationUtil.notificationChannelReminders); + expect(details.channelName, AppLocalizations().remindersNotificationChannelName); + expect(details.channelDescription, AppLocalizations().remindersNotificationChannelDescription); verify(analytics.logEvent(AnalyticsEventConstants.REMINDER_EVENT_CREATE)); }); @@ -177,21 +174,29 @@ void main() { var d = reminder.date!.toUtc(); var date = tz.TZDateTime.utc(d.year, d.month, d.day, d.hour, d.minute, d.second); - final NotificationDetails details = verify(plugin.zonedSchedule( + final AndroidNotificationDetails details = verify(plugin.zonedSchedule( reminder.id, 'title', 'body', date, captureAny, + scheduleMode: AndroidScheduleMode.exactAllowWhileIdle, payload: json.encode(serialize(expectedPayload)), - uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, )).captured.first; - expect(details.iOS, isNull); - expect(details.android?.channelId, NotificationUtil.notificationChannelReminders); - expect(details.android?.channelName, AppLocalizations().remindersNotificationChannelName); - expect(details.android?.channelDescription, AppLocalizations().remindersNotificationChannelDescription); + expect(details.channelId, NotificationUtil.notificationChannelReminders); + expect(details.channelName, AppLocalizations().remindersNotificationChannelName); + expect(details.channelDescription, AppLocalizations().remindersNotificationChannelDescription); verify(analytics.logEvent(AnalyticsEventConstants.REMINDER_ASSIGNMENT_CREATE)); }); + + test('Request exact alarm permission', () async { + when(plugin.requestExactAlarmsPermission()).thenAnswer((_) => Future.value(true)); + + final result = await NotificationUtil().requestScheduleExactAlarmPermission(); + + expect(result, true); + verify(plugin.requestExactAlarmsPermission()); + }); } diff --git a/apps/flutter_parent/test/utils/test_app.dart b/apps/flutter_parent/test/utils/test_app.dart index f477beeab6..62d116d4b4 100644 --- a/apps/flutter_parent/test/utils/test_app.dart +++ b/apps/flutter_parent/test/utils/test_app.dart @@ -12,8 +12,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'dart:ui'; - import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -30,12 +28,13 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/design/theme_prefs.dart'; import 'package:flutter_parent/utils/remote_config_utils.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_test/src/deprecated.dart'; import 'package:get_it/get_it.dart'; +import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'platform_config.dart'; -import 'test_helpers/mock_helpers.dart'; import 'test_helpers/mock_helpers.mocks.dart'; class TestApp extends StatefulWidget { @@ -259,28 +258,33 @@ Future setupPlatformChannels({PlatformConfig config = const PlatformConfig } } -/// WebView helpers. These are needed as web views tie into platform views. These are special though as the channel -/// name depends on the platform view's ID. This makes mocking these generically difficult as each id has a different -/// platform channel to register. +/// WebView helpers. These are needed as web views tie into platform views. /// /// Inspired solution is a slimmed down version of the WebView test: -/// https://github.com/flutter/plugins/blob/3b71d6e9a4456505f0b079074fcbc9ba9f8e0e15/packages/webview_flutter/test/webview_flutter_test.dart +/// https://github.com/flutter/plugins/blob/webview_flutter-v3.0.4/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart void _initPlatformWebView() { - const MethodChannel('plugins.flutter.io/cookie_manager', const StandardMethodCodec()) - .setMockMethodCallHandler((_) => Future.sync(() => false)); - - // Intercept when a web view is getting created so we can set up the platform channel - SystemChannels.platform_views.setMockMethodCallHandler((call) { - switch (call.method) { - case 'create': - final id = call.arguments['id']; - MethodChannel('plugins.flutter.io/webview_$id', const StandardMethodCodec()) - .setMockMethodCallHandler((_) => Future.sync(() {})); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } + final mockWebViewPlatformController = MockWebViewPlatformController(); + final mockWebViewPlatform = MockWebViewPlatform(); + when(mockWebViewPlatform.build( + context: anyNamed('context'), + creationParams: anyNamed('creationParams'), + webViewPlatformCallbacksHandler: + anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: anyNamed('gestureRecognizers'), + )).thenAnswer((Invocation invocation) { + final WebViewPlatformCreatedCallback onWebViewPlatformCreated = + invocation.namedArguments[const Symbol('onWebViewPlatformCreated')] + as WebViewPlatformCreatedCallback; + return TestPlatformWebView( + mockWebViewPlatformController: mockWebViewPlatformController, + onWebViewPlatformCreated: onWebViewPlatformCreated, + ); }); + + WebView.platform = mockWebViewPlatform; + WebViewCookieManagerPlatform.instance = FakeWebViewCookieManager(); } /// Mocks the platform channel used by the package_info plugin @@ -391,4 +395,47 @@ void _initPathProvider() { } return null; }); +} + +class TestPlatformWebView extends StatefulWidget { + const TestPlatformWebView({ + Key? key, + required this.mockWebViewPlatformController, + this.onWebViewPlatformCreated, + }) : super(key: key); + + final MockWebViewPlatformController mockWebViewPlatformController; + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated; + + @override + State createState() => TestPlatformWebViewState(); +} + +class TestPlatformWebViewState extends State { + @override + void initState() { + super.initState(); + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated = + widget.onWebViewPlatformCreated; + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(widget.mockWebViewPlatformController); + } + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} + +class FakeWebViewCookieManager extends WebViewCookieManagerPlatform { + @override + Future clearCookies() { + return Future.value(false); + } + + @override + Future setCookie(WebViewCookie cookie) { + return Future.value(null); + } } \ No newline at end of file diff --git a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart index 99e7339c43..a7b3985f1e 100644 --- a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart +++ b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart @@ -19,9 +19,7 @@ // settings will correspond the specified values. import 'dart:io'; -import 'package:barcode_scan2/platform_wrapper.dart'; import 'package:dio/dio.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; @@ -45,7 +43,6 @@ import 'package:flutter_parent/network/api/planner_api.dart'; import 'package:flutter_parent/network/api/user_api.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/network/utils/authentication_interceptor.dart'; -import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/screens/account_creation/account_creation_interactor.dart'; import 'package:flutter_parent/screens/alert_thresholds/alert_thresholds_interactor.dart'; import 'package:flutter_parent/screens/alerts/alerts_interactor.dart'; @@ -60,7 +57,6 @@ import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shel import 'package:flutter_parent/screens/dashboard/alert_notifier.dart'; import 'package:flutter_parent/screens/dashboard/dashboard_interactor.dart'; import 'package:flutter_parent/screens/dashboard/inbox_notifier.dart'; -import 'package:flutter_parent/screens/dashboard/selected_student_notifier.dart'; import 'package:flutter_parent/screens/domain_search/domain_search_interactor.dart'; import 'package:flutter_parent/screens/events/event_details_interactor.dart'; import 'package:flutter_parent/screens/help/help_screen_interactor.dart'; @@ -102,6 +98,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:sqflite/sqflite.dart'; import 'package:video_player/video_player.dart'; +import 'package:webview_flutter/webview_flutter.dart'; @GenerateNiceMocks([ MockSpec(), @@ -140,7 +137,7 @@ import 'package:video_player/video_player.dart'; MockSpec(), MockSpec(), MockSpec(), - MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -189,6 +186,8 @@ import 'package:video_player/video_player.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec() ]) import 'mock_helpers.mocks.dart'; diff --git a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart index 326113c085..6b15d639d3 100644 --- a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart +++ b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart @@ -20,6 +20,7 @@ import 'package:firebase_remote_config_platform_interface/firebase_remote_config as _i14; import 'package:fluro/fluro.dart' as _i84; import 'package:flutter/foundation.dart' as _i11; +import 'package:flutter/gestures.dart' as _i142; import 'package:flutter/material.dart' as _i17; import 'package:flutter/services.dart' as _i15; import 'package:flutter_downloader/flutter_downloader.dart' as _i133; @@ -5982,6 +5983,16 @@ class MockNotificationUtil extends _i1.Mock implements _i75.NotificationUtil { returnValue: _i8.Future.value(), returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); + @override + _i8.Future requestScheduleExactAlarmPermission() => + (super.noSuchMethod( + Invocation.method( + #requestScheduleExactAlarmPermission, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); } /// A class which mocks [OAuthApi]. @@ -6058,14 +6069,14 @@ class MockPageApi extends _i1.Mock implements _i79.PageApi { ) as _i8.Future<_i59.CanvasPage?>); } -/// A class which mocks [FlutterLocalNotificationsPlugin]. +/// A class which mocks [AndroidFlutterLocalNotificationsPlugin]. /// /// See the documentation for Mockito's code generation for more information. -class MockFlutterLocalNotificationsPlugin extends _i1.Mock - implements _i80.FlutterLocalNotificationsPlugin { +class MockAndroidFlutterLocalNotificationsPlugin extends _i1.Mock + implements _i80.AndroidFlutterLocalNotificationsPlugin { @override - _i8.Future initialize( - _i80.InitializationSettings? initializationSettings, { + _i8.Future initialize( + _i80.AndroidInitializationSettings? initializationSettings, { _i80.DidReceiveNotificationResponseCallback? onDidReceiveNotificationResponse, _i80.DidReceiveBackgroundNotificationResponseCallback? @@ -6081,96 +6092,114 @@ class MockFlutterLocalNotificationsPlugin extends _i1.Mock onDidReceiveBackgroundNotificationResponse, }, ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future requestExactAlarmsPermission() => (super.noSuchMethod( + Invocation.method( + #requestExactAlarmsPermission, + [], + ), returnValue: _i8.Future.value(), returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); @override - _i8.Future<_i80.NotificationAppLaunchDetails?> - getNotificationAppLaunchDetails() => (super.noSuchMethod( - Invocation.method( - #getNotificationAppLaunchDetails, - [], - ), - returnValue: _i8.Future<_i80.NotificationAppLaunchDetails?>.value(), - returnValueForMissingStub: - _i8.Future<_i80.NotificationAppLaunchDetails?>.value(), - ) as _i8.Future<_i80.NotificationAppLaunchDetails?>); + _i8.Future requestNotificationsPermission() => (super.noSuchMethod( + Invocation.method( + #requestNotificationsPermission, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override - _i8.Future show( + _i8.Future zonedSchedule( int? id, String? title, String? body, - _i80.NotificationDetails? notificationDetails, { + _i81.TZDateTime? scheduledDate, + _i80.AndroidNotificationDetails? notificationDetails, { + required _i80.AndroidScheduleMode? scheduleMode, String? payload, + _i80.DateTimeComponents? matchDateTimeComponents, }) => (super.noSuchMethod( Invocation.method( - #show, + #zonedSchedule, [ id, title, body, + scheduledDate, notificationDetails, ], - {#payload: payload}, + { + #scheduleMode: scheduleMode, + #payload: payload, + #matchDateTimeComponents: matchDateTimeComponents, + }, ), returnValue: _i8.Future.value(), returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); @override - _i8.Future cancel( - int? id, { - String? tag, + _i8.Future startForegroundService( + int? id, + String? title, + String? body, { + _i80.AndroidNotificationDetails? notificationDetails, + String? payload, + _i80.AndroidServiceStartType? startType = + _i80.AndroidServiceStartType.startSticky, + Set<_i80.AndroidServiceForegroundType>? foregroundServiceTypes, }) => (super.noSuchMethod( Invocation.method( - #cancel, - [id], - {#tag: tag}, + #startForegroundService, + [ + id, + title, + body, + ], + { + #notificationDetails: notificationDetails, + #payload: payload, + #startType: startType, + #foregroundServiceTypes: foregroundServiceTypes, + }, ), returnValue: _i8.Future.value(), returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); @override - _i8.Future cancelAll() => (super.noSuchMethod( + _i8.Future stopForegroundService() => (super.noSuchMethod( Invocation.method( - #cancelAll, + #stopForegroundService, [], ), returnValue: _i8.Future.value(), returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); @override - _i8.Future zonedSchedule( + _i8.Future show( int? id, String? title, - String? body, - _i81.TZDateTime? scheduledDate, - _i80.NotificationDetails? notificationDetails, { - required _i80.UILocalNotificationDateInterpretation? - uiLocalNotificationDateInterpretation, - bool? androidAllowWhileIdle = false, - _i80.AndroidScheduleMode? androidScheduleMode, + String? body, { + _i80.AndroidNotificationDetails? notificationDetails, String? payload, - _i80.DateTimeComponents? matchDateTimeComponents, }) => (super.noSuchMethod( Invocation.method( - #zonedSchedule, + #show, [ id, title, body, - scheduledDate, - notificationDetails, ], { - #uiLocalNotificationDateInterpretation: - uiLocalNotificationDateInterpretation, - #androidAllowWhileIdle: androidAllowWhileIdle, - #androidScheduleMode: androidScheduleMode, + #notificationDetails: notificationDetails, #payload: payload, - #matchDateTimeComponents: matchDateTimeComponents, }, ), returnValue: _i8.Future.value(), @@ -6181,11 +6210,10 @@ class MockFlutterLocalNotificationsPlugin extends _i1.Mock int? id, String? title, String? body, - _i80.RepeatInterval? repeatInterval, - _i80.NotificationDetails? notificationDetails, { + _i80.RepeatInterval? repeatInterval, { + _i80.AndroidNotificationDetails? notificationDetails, String? payload, - bool? androidAllowWhileIdle = false, - _i80.AndroidScheduleMode? androidScheduleMode, + _i80.AndroidScheduleMode? scheduleMode = _i80.AndroidScheduleMode.exact, }) => (super.noSuchMethod( Invocation.method( @@ -6195,18 +6223,139 @@ class MockFlutterLocalNotificationsPlugin extends _i1.Mock title, body, repeatInterval, - notificationDetails, ], { + #notificationDetails: notificationDetails, #payload: payload, - #androidAllowWhileIdle: androidAllowWhileIdle, - #androidScheduleMode: androidScheduleMode, + #scheduleMode: scheduleMode, }, ), returnValue: _i8.Future.value(), returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); @override + _i8.Future cancel( + int? id, { + String? tag, + }) => + (super.noSuchMethod( + Invocation.method( + #cancel, + [id], + {#tag: tag}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future createNotificationChannelGroup( + _i80.AndroidNotificationChannelGroup? notificationChannelGroup) => + (super.noSuchMethod( + Invocation.method( + #createNotificationChannelGroup, + [notificationChannelGroup], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future deleteNotificationChannelGroup(String? groupId) => + (super.noSuchMethod( + Invocation.method( + #deleteNotificationChannelGroup, + [groupId], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future createNotificationChannel( + _i80.AndroidNotificationChannel? notificationChannel) => + (super.noSuchMethod( + Invocation.method( + #createNotificationChannel, + [notificationChannel], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future deleteNotificationChannel(String? channelId) => + (super.noSuchMethod( + Invocation.method( + #deleteNotificationChannel, + [channelId], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future<_i80.MessagingStyleInformation?> + getActiveNotificationMessagingStyle( + int? id, { + String? tag, + }) => + (super.noSuchMethod( + Invocation.method( + #getActiveNotificationMessagingStyle, + [id], + {#tag: tag}, + ), + returnValue: _i8.Future<_i80.MessagingStyleInformation?>.value(), + returnValueForMissingStub: + _i8.Future<_i80.MessagingStyleInformation?>.value(), + ) as _i8.Future<_i80.MessagingStyleInformation?>); + @override + _i8.Future?> + getNotificationChannels() => (super.noSuchMethod( + Invocation.method( + #getNotificationChannels, + [], + ), + returnValue: + _i8.Future?>.value(), + returnValueForMissingStub: + _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future areNotificationsEnabled() => (super.noSuchMethod( + Invocation.method( + #areNotificationsEnabled, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future canScheduleExactNotifications() => (super.noSuchMethod( + Invocation.method( + #canScheduleExactNotifications, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future cancelAll() => (super.noSuchMethod( + Invocation.method( + #cancelAll, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future<_i80.NotificationAppLaunchDetails?> + getNotificationAppLaunchDetails() => (super.noSuchMethod( + Invocation.method( + #getNotificationAppLaunchDetails, + [], + ), + returnValue: _i8.Future<_i80.NotificationAppLaunchDetails?>.value(), + returnValueForMissingStub: + _i8.Future<_i80.NotificationAppLaunchDetails?>.value(), + ) as _i8.Future<_i80.NotificationAppLaunchDetails?>); + @override _i8.Future> pendingNotificationRequests() => (super.noSuchMethod( Invocation.method( @@ -8209,6 +8358,14 @@ class MockSettingsInteractor extends _i1.Mock returnValueForMissingStub: null, ); @override + void routeToLegal(_i17.BuildContext? context) => super.noSuchMethod( + Invocation.method( + #routeToLegal, + [context], + ), + returnValueForMissingStub: null, + ); + @override void toggleDarkMode( _i17.BuildContext? context, _i17.GlobalKey<_i17.State<_i17.StatefulWidget>>? anchorKey, @@ -9040,3 +9197,319 @@ class MockRemoteConfigInteractor extends _i1.Mock returnValueForMissingStub: null, ); } + +/// A class which mocks [WebViewPlatformController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformController extends _i1.Mock + implements _i16.WebViewPlatformController { + @override + _i8.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method( + #loadFile, + [absoluteFilePath], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future loadHtmlString( + String? html, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [html], + {#baseUrl: baseUrl}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future loadUrl( + String? url, + Map? headers, + ) => + (super.noSuchMethod( + Invocation.method( + #loadUrl, + [ + url, + headers, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future loadRequest(_i16.WebViewRequest? request) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [request], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future updateSettings(_i16.WebSettings? setting) => + (super.noSuchMethod( + Invocation.method( + #updateSettings, + [setting], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future currentUrl() => (super.noSuchMethod( + Invocation.method( + #currentUrl, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future clearCache() => (super.noSuchMethod( + Invocation.method( + #clearCache, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future evaluateJavascript(String? javascript) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, + [javascript], + ), + returnValue: _i8.Future.value(''), + returnValueForMissingStub: _i8.Future.value(''), + ) as _i8.Future); + @override + _i8.Future runJavascript(String? javascript) => (super.noSuchMethod( + Invocation.method( + #runJavascript, + [javascript], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future runJavascriptReturningResult(String? javascript) => + (super.noSuchMethod( + Invocation.method( + #runJavascriptReturningResult, + [javascript], + ), + returnValue: _i8.Future.value(''), + returnValueForMissingStub: _i8.Future.value(''), + ) as _i8.Future); + @override + _i8.Future addJavascriptChannels(Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #addJavascriptChannels, + [javascriptChannelNames], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future removeJavascriptChannels( + Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #removeJavascriptChannels, + [javascriptChannelNames], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future getScrollX() => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future getScrollY() => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); +} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i16.WebViewPlatform { + @override + _i17.Widget build({ + required _i17.BuildContext? context, + required _i16.CreationParams? creationParams, + required _i16.WebViewPlatformCallbacksHandler? + webViewPlatformCallbacksHandler, + required _i16.JavascriptChannelRegistry? javascriptChannelRegistry, + _i16.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set<_i11.Factory<_i142.OneSequenceGestureRecognizer>>? gestureRecognizers, + }) => + (super.noSuchMethod( + Invocation.method( + #build, + [], + { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers, + }, + ), + returnValue: _FakeWidget_30( + this, + Invocation.method( + #build, + [], + { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers, + }, + ), + ), + returnValueForMissingStub: _FakeWidget_30( + this, + Invocation.method( + #build, + [], + { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers, + }, + ), + ), + ) as _i17.Widget); + @override + _i8.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt index d40df12d12..599a31aa0a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt @@ -55,8 +55,8 @@ class DashboardE2EOfflineTest : StudentTest() { dashboardPage.openGlobalManageOfflineContentPage() Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") - manageOfflineContentPage.selectEntireCourseForSync(course1.name) - manageOfflineContentPage.clickOnSyncButton() + manageOfflineContentPage.changeItemSelectionState(course1.name) + manageOfflineContentPage.clickOnSyncButtonAndConfirm() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt new file mode 100644 index 0000000000..5ca6d91392 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2023 - 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.student.ui.e2e.offline + +import android.util.Log +import androidx.test.espresso.Espresso +import com.google.android.material.checkbox.MaterialCheckBox +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test + +@HiltAndroidTest +class ManageOfflineContentE2ETest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.OFFLINE_CONTENT, TestCategory.E2E) + fun testManageOfflineContentE2ETest() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1) + val student = data.studentsList[0] + val course1 = data.coursesList[0] + val course2 = data.coursesList[1] + val testAnnouncement = data.announcementsList[0] + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.clickCourseOverflowMenu(course1.name, "Manage Offline Content") + + Log.d(STEP_TAG, "Assert that if there is nothing selected yet, the 'SELECT ALL' button text will be displayed on the top-left corner of the toolbar.") + manageOfflineContentPage.assertSelectButtonText(selectAll = true) + + Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state is 'Unchecked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync.") + manageOfflineContentPage.changeItemSelectionState(course1.name) + + Log.d(STEP_TAG, "Assert that if there is something selected yet, the 'DESELECT ALL' button text will be displayed on the top-left corner of the toolbar.") + manageOfflineContentPage.assertSelectButtonText(selectAll = false) + + Log.d(STEP_TAG, "Assert that the Storage info details are displayed properly.") + manageOfflineContentPage.assertStorageInfoDetails() + + Log.d(STEP_TAG, "Assert that the tool bar texts are displayed properly, so the subtitle is '${course1.name}', because we are on the Manage Offline Content page of '${course1.name}' course.") + manageOfflineContentPage.assertToolbarTexts(course1.name) + + Log.d(STEP_TAG, "Deselect the 'Announcements' and 'Discussions' of the '${course1.name}' course.") + manageOfflineContentPage.changeItemSelectionState("Announcements") + manageOfflineContentPage.changeItemSelectionState("Discussions") + + Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state is 'Indeterminate'.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_INDETERMINATE) + + Log.d(STEP_TAG, "Select the entire '${course1.name}' course (again) for sync.") + manageOfflineContentPage.changeItemSelectionState(course1.name) + + Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state and became 'Checked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_CHECKED) + + Log.d(STEP_TAG, "Assert that the previously unchecked 'Announcements' and 'Discussions' checkboxes are became 'Checked'.") + manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_CHECKED) + manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_CHECKED) + + Log.d(STEP_TAG, "Assert that the 'DESELECT ALL' button is still displayed because there are still more than zero item checked.") + manageOfflineContentPage.assertSelectButtonText(selectAll = false) + + Log.d(STEP_TAG, "Deselect '${course1.name}' course's checkbox.") + manageOfflineContentPage.changeItemSelectionState(course1.name) + + Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state and became 'Unchecked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Assert that the previously unchecked 'Announcements' and 'Discussions' checkboxes are became 'Unchecked'.") + manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Assert that the 'SELECT ALL' button is displayed because there is no item checked.") + manageOfflineContentPage.assertSelectButtonText(selectAll = true) + + Log.d(STEP_TAG, "Click on 'SELECT ALL' button.") + manageOfflineContentPage.clickOnSelectAllButton() + + Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state and became 'Checked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_CHECKED) + + Log.d(STEP_TAG, "Assert that the previously unchecked 'Announcements' and 'Discussions' checkboxes are became 'Checked'.") + manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_CHECKED) + manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_CHECKED) + + Log.d(STEP_TAG, "Assert that the 'DESELECT ALL' will be displayed after clicking the 'SELECT ALL' button.") + manageOfflineContentPage.assertSelectButtonText(selectAll = false) + + Log.d(STEP_TAG, "Click on 'DESELECT ALL' button.") + manageOfflineContentPage.clickOnDeselectAllButton() + + Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state that it became 'Unchecked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Assert that the previously checked 'Announcements' and 'Discussions' checkboxes are became 'Unchecked'.") + manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Assert that the 'SELECT ALL' will be displayed after clicking the 'SELECT ALL' button.") + manageOfflineContentPage.assertSelectButtonText(selectAll = true) + + Log.d(STEP_TAG, "Navigate back to Dashboard Page. Open 'Global' Manage Offline Content page.") + Espresso.pressBack() + dashboardPage.openGlobalManageOfflineContentPage() + + Log.d(STEP_TAG, "Assert that the Storage info details are displayed properly.") + manageOfflineContentPage.assertStorageInfoDetails() + + Log.d(STEP_TAG, "Assert that the tool bar texts are displayed properly, so the subtitle is 'All Courses', because we are on the 'Global' Manage Offline Content page.") + manageOfflineContentPage.assertToolbarTexts("All Courses") + + Log.d(STEP_TAG, "Assert that the '${course1.name}' and '${course2.name}' courses' checkboxes are 'Unchecked' yet.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.assertCheckedStateOfItem(course2.name, MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Expand '${course1.name}' course.") + manageOfflineContentPage.expandCollapseItem(course1.name) + + Log.d(STEP_TAG, "Assert that the 'Announcements' and 'Discussions' items are 'unchecked' (and so all the other tabs of the course) because the course is NOT selected.") + manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Collapse '${course1.name}' course.") + manageOfflineContentPage.expandCollapseItem(course1.name) + + manageOfflineContentPage.waitForItemDisappear("Announcements") + manageOfflineContentPage.waitForItemDisappear("Discussions") + + Thread.sleep(1000) //need to wait 1 second here because sometimes expand/collapse happens too fast + Log.d(STEP_TAG, "Expand '${course2.name}' course.") + manageOfflineContentPage.expandCollapseItem(course2.name) + + Log.d(STEP_TAG, "Assert that the 'Grades' and 'Discussions' items are 'Unchecked' (and so all the other tabs of the course) because the course has not selected.") + manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.assertCheckedStateOfItem("Grades", MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Check '${course2.name}' course.") + manageOfflineContentPage.changeItemSelectionState(course2.name) + + Log.d(STEP_TAG, "Assert that the '${course2.name}' course's checkbox state and became 'Checked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course2.name, MaterialCheckBox.STATE_CHECKED) + + Log.d(STEP_TAG, "Assert that the 'Grades' and 'Discussions' items are 'checked' (and so all the other tabs of the course) because the course has selected.") + manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_CHECKED) + manageOfflineContentPage.assertCheckedStateOfItem("Grades", MaterialCheckBox.STATE_CHECKED) + + Log.d(STEP_TAG, "Collapse '${course2.name}' course.") + manageOfflineContentPage.expandCollapseItem(course2.name) + + Log.d(STEP_TAG, "Assert that both of the seeded courses are displayed as a selectable item in the Manage Offline Content page.") + manageOfflineContentPage.assertCourseCountWithMatcher(2) + + Log.d(STEP_TAG, "Click on the 'Sync' button.") + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + OfflineTestUtils.turnOffConnectionViaADB() + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Select '${course2.name}' course and open 'Grades' menu to check if it's really synced and can be seen in offline mode.") + dashboardPage.selectCourse(course2) + courseBrowserPage.selectGrades() + + Log.d(STEP_TAG,"Assert that the empty view is displayed on the 'Grades' page (just to check that it's available in offline mode.") + courseGradesPage.assertEmptyView() + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") + OfflineTestUtils.turnOnConnectionViaADB() + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt index 7c3413a09f..77cea19bc2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt @@ -57,8 +57,8 @@ class OfflineSyncProgressE2ETest : StudentTest() { dashboardPage.openGlobalManageOfflineContentPage() Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") - manageOfflineContentPage.selectEntireCourseForSync(course1.name) - manageOfflineContentPage.clickOnSyncButton() + manageOfflineContentPage.changeItemSelectionState(course1.name) + manageOfflineContentPage.clickOnSyncButtonAndConfirm() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt index a0e3049b48..8ae21f49ac 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt @@ -19,18 +19,8 @@ package com.instructure.student.ui.interaction import android.os.SystemClock.sleep import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.CanvasContextPermission -import com.instructure.canvasapi2.models.CourseSettings -import com.instructure.canvasapi2.models.DiscussionEntry -import com.instructure.canvasapi2.models.RemoteFile -import com.instructure.canvasapi2.models.Tab +import com.instructure.canvas.espresso.mockCanvas.* +import com.instructure.canvasapi2.models.* import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -761,7 +751,7 @@ class DiscussionsInteractionTest : StudentTest() { if (enableDiscussionTopicCreation) { data.courses.values.forEach { course -> - course.permissions = CanvasContextPermission(canCreateDiscussionTopic = true) + data.addCoursePermissions(course.id, CanvasContextPermission(canCreateDiscussionTopic = true)) } } val course1 = data.courses.values.first() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt new file mode 100644 index 0000000000..2be3ec69ac --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2023 - 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.student.ui.interaction + +import android.text.format.Formatter +import androidx.test.espresso.Espresso +import com.google.android.material.checkbox.MaterialCheckBox +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addFileToCourse +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Tab +import com.instructure.dataseeding.util.Randomizer +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.pandautils.R +import com.instructure.pandautils.utils.StorageUtils +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class OfflineContentInteractionTest : StudentTest() { + + @Inject + lateinit var storageUtils: StorageUtils + + override fun displaysPageObjects() = Unit + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun displaysNoCourses() { + goToOfflineContent(createMockCanvas(courseCount = 0)) + manageOfflineContentPage.assertDisplaysNoCourses() + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun displaysEmptyCourse() { + val data = createMockCanvas(courseCount = 1, hasTabs = false) + goToOfflineContent(data) + manageOfflineContentPage.expandCollapseItem(data.courses.values.first().name) + manageOfflineContentPage.assertDisplaysEmptyCourse() + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun displaysCoursesCollapsedIfGlobalOfflineContent() { + val data = createMockCanvas() + goToOfflineContent(data) + manageOfflineContentPage.assertDisplaysItemWithExpandedState(data.courses.values.first().name, false) + manageOfflineContentPage.assertDisplaysItemWithExpandedState(data.courses.values.last().name, false) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun displaysCourseExpandedIfCourseOfflineContent() { + val data = createMockCanvas() + goToOfflineContentByCourse(data) + manageOfflineContentPage.assertDisplaysItemWithExpandedState(data.courses.values.first().name, true) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun displaysCourseTabsAndFiles() { + val data = createMockCanvas(courseCount = 1) + goToOfflineContent(data) + val course = data.courses.values.first() + manageOfflineContentPage.expandCollapseItem(course.name) + getCourseItemNames(data, course).forEach { manageOfflineContentPage.assertItemDisplayed(it) } + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun expandCourse() { + val data = createMockCanvas() + goToOfflineContent(data) + val course = data.courses.values.first() + manageOfflineContentPage.assertDisplaysItemWithExpandedState(course.name, false) + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.assertDisplaysItemWithExpandedState(course.name, true) + manageOfflineContentPage.assertItemDisplayed(data.courseTabs[course.id]!!.first().label!!) + getCourseItemNames(data, course).forEach { manageOfflineContentPage.assertItemDisplayed(it) } + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun selectCourse() { + val data = createMockCanvas() + goToOfflineContent(data) + val course = data.courses.values.first() + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.changeItemSelectionState(course.name) + getCourseItemNames(data, course).forEach { manageOfflineContentPage.assertCheckedStateOfItem(it, MaterialCheckBox.STATE_CHECKED) } + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun selectTab() { + val data = createMockCanvas() + goToOfflineContent(data) + val course = data.courses.values.first() + val firstTabName = data.courseTabs[course.id]!!.map { it.label!! }.first() + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.changeItemSelectionState(firstTabName) + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_INDETERMINATE) + manageOfflineContentPage.assertCheckedStateOfItem(firstTabName, MaterialCheckBox.STATE_CHECKED) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun expandFilesTab() { + val data = createMockCanvas() + goToOfflineContent(data) + val course = data.courses.values.first() + val filesTabName = data.courseTabs[course.id]!!.find { it.tabId == Tab.FILES_ID }!!.label!! + val firstFileName = data.folderFiles[data.courseRootFolders[course.id]!!.id]!!.map { it.displayName!! }.first() + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.expandCollapseItem(filesTabName) + manageOfflineContentPage.assertDisplaysItemWithExpandedState(filesTabName, false) + manageOfflineContentPage.expandCollapseItem(filesTabName) + manageOfflineContentPage.assertItemDisplayed(firstFileName) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun selectFilesTab() { + val data = createMockCanvas() + goToOfflineContent(data) + val course = data.courses.values.first() + val filesTabName = data.courseTabs[course.id]!!.find { it.tabId == Tab.FILES_ID }!!.label!! + val fileNames = data.folderFiles[data.courseRootFolders[course.id]!!.id]!!.map { it.displayName!! } + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.assertCheckedStateOfItem(filesTabName, MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.changeItemSelectionState(filesTabName) + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_INDETERMINATE) + manageOfflineContentPage.assertCheckedStateOfItem(filesTabName, MaterialCheckBox.STATE_CHECKED) + fileNames.forEach { manageOfflineContentPage.assertCheckedStateOfItem(it, MaterialCheckBox.STATE_CHECKED) } + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun selectFile() { + val data = createMockCanvas() + goToOfflineContent(data) + val course = data.courses.values.first() + val filesTabName = data.courseTabs[course.id]!!.find { it.tabId == Tab.FILES_ID }!!.label!! + val firstFileName = data.folderFiles[data.courseRootFolders[course.id]!!.id]!!.map { it.displayName!! }.first() + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.assertCheckedStateOfItem(filesTabName, MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.changeItemSelectionState(firstFileName) + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_INDETERMINATE) + manageOfflineContentPage.assertCheckedStateOfItem(filesTabName, MaterialCheckBox.STATE_INDETERMINATE) + manageOfflineContentPage.assertCheckedStateOfItem(firstFileName, MaterialCheckBox.STATE_CHECKED) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun selectAllFiles() { + val data = createMockCanvas(courseCount = 1) + goToOfflineContent(data) + val course = data.courses.values.first() + val filesTabName = data.courseTabs[course.id]!!.find { it.tabId == Tab.FILES_ID }!!.label!! + val fileNames = data.folderFiles[data.courseRootFolders[course.id]!!.id]!!.map { it.displayName!! } + manageOfflineContentPage.expandCollapseItem(course.name) + fileNames.forEachIndexed { index, file -> + manageOfflineContentPage.changeItemSelectionState(file) + manageOfflineContentPage.assertCheckedStateOfItem( + filesTabName, + if (index == fileNames.size - 1) MaterialCheckBox.STATE_CHECKED else MaterialCheckBox.STATE_INDETERMINATE + ) + } + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_INDETERMINATE) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun selectAllTabs() { + val data = createMockCanvas() + goToOfflineContent(data) + val course = data.courses.values.first() + val tabNames = data.courseTabs[course.id]!!.map { it.label!! } + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_UNCHECKED) + tabNames.forEachIndexed { index, tab -> + manageOfflineContentPage.changeItemSelectionState(tab) + manageOfflineContentPage.assertCheckedStateOfItem( + course.name, + if (index == tabNames.size - 1) MaterialCheckBox.STATE_CHECKED else MaterialCheckBox.STATE_INDETERMINATE + ) + } + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun selectAllToggle() { + val data = createMockCanvas() + goToOfflineContent(data) + manageOfflineContentPage.clickOnSelectAllButton() + data.courses.values.forEach { + manageOfflineContentPage.assertCheckedStateOfItem(it.name, MaterialCheckBox.STATE_CHECKED) + } + manageOfflineContentPage.clickOnDeselectAllButton() + data.courses.values.forEach { + manageOfflineContentPage.assertCheckedStateOfItem(it.name, MaterialCheckBox.STATE_UNCHECKED) + } + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun displaysDiscardDialogIfNeeded() { + goToOfflineContent() + Espresso.pressBack() + dashboardPage.openGlobalManageOfflineContentPage() + manageOfflineContentPage.clickOnSelectAllButton() + Espresso.pressBack() + manageOfflineContentPage.assertDiscardDialogDisplayed() + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun displaysWifiOnlySyncDialog() { + val data = createMockCanvas() + val course = data.courses.values.first() + goToOfflineContent(data) + manageOfflineContentPage.changeItemSelectionState(course.name) + manageOfflineContentPage.clickOnSyncButton() + manageOfflineContentPage.assertSyncDialogDisplayed( + activityRule.activity.getString( + R.string.offline_content_sync_dialog_message_wifi_only, + Formatter.formatShortFileSize(activityRule.activity, getCourseContentSize(data, course)) + ) + ) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun displaysSyncDialog() { + val data = createMockCanvas() + val course = data.courses.values.first() + setupSyncAndGoToOfflineContent(data) + manageOfflineContentPage.changeItemSelectionState(course.name) + manageOfflineContentPage.clickOnSyncButton() + manageOfflineContentPage.assertSyncDialogDisplayed( + activityRule.activity.getString( + R.string.offline_content_sync_dialog_message, + Formatter.formatShortFileSize(activityRule.activity, getCourseContentSize(data, course)) + ) + ) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun savesChangesOnSync() { + val data = createMockCanvas() + goToOfflineContent(data) + manageOfflineContentPage.clickOnSelectAllButton() + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + dashboardPage.openGlobalManageOfflineContentPage() + data.courses.values.forEach { manageOfflineContentPage.assertCheckedStateOfItem(it.name, MaterialCheckBox.STATE_CHECKED) } + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) + fun calculatesStorageInfoCorrectly() { + val data = createMockCanvas(fileCount = 10, largeFiles = true) + val course = data.courses.values.first() + goToOfflineContent(data) + val total = storageUtils.getTotalSpace() + val used = total - storageUtils.getFreeSpace() + manageOfflineContentPage.assertStorageInfoDetails() + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.assertStorageInfoText( + activityRule.activity.getString( + R.string.offline_content_storage_info, + Formatter.formatShortFileSize(activityRule.activity, used), + Formatter.formatShortFileSize(activityRule.activity, total) + ) + ) + manageOfflineContentPage.changeItemSelectionState(course.name) + manageOfflineContentPage.assertStorageInfoText( + activityRule.activity.getString( + R.string.offline_content_storage_info, + Formatter.formatShortFileSize(activityRule.activity, used + getCourseContentSize(data, course)), + Formatter.formatShortFileSize(activityRule.activity, total) + ) + ) + } + + private fun createMockCanvas(courseCount: Int = 2, hasTabs: Boolean = true, fileCount: Int = 3, largeFiles: Boolean = false): MockCanvas { + val data = MockCanvas.init(studentCount = 1, teacherCount = 1, courseCount = courseCount) + data.offlineModeEnabled = true + + if (hasTabs) { + val filesTab = Tab(position = 2, label = "Files", visibility = "public", tabId = Tab.FILES_ID) + + data.courses.forEach { course -> + val courseId = course.value.id + + data.courseTabs[courseId]?.add(filesTab) + + repeat(fileCount) { + data.addFileToCourse( + courseId = courseId, + displayName = "test-${courseId}-${it}.pdf", + contentType = "application/pdf", + fileContent = if (largeFiles) Randomizer.randomLargeTextFileContents() else Randomizer.randomTextFileContents() + ) + } + } + } else { + data.courseTabs.clear() + } + + return data + } + + private fun goToOfflineContent(data: MockCanvas = createMockCanvas()) { + val student = data.users.values.first() + val token = data.tokenFor(student).orEmpty() + tokenLogin(data.domain, token, student) + dashboardPage.openGlobalManageOfflineContentPage() + } + + private fun goToOfflineContentByCourse(data: MockCanvas = createMockCanvas()) { + val student = data.students.first() + val token = data.tokenFor(student).orEmpty() + tokenLogin(data.domain, token, student) + dashboardPage.clickCourseOverflowMenu(data.courses.values.first().name, "Manage Offline Content") + } + + private fun getCourseItemNames(data: MockCanvas, course: Course): List { + return data.courseTabs[course.id]!!.map { it.label!! } + course.name + + data.folderFiles[data.courseRootFolders[course.id]!!.id]!!.map { it.displayName!! } + } + + private fun getCourseContentSize(data: MockCanvas, course: Course): Long { + return data.folderFiles[data.courseRootFolders[course.id]!!.id]!!.sumOf { it.size } + + data.courseTabs[course.id]!!.filter { it.tabId != Tab.FILES_ID }.size * 100000 + } + + private fun setupSyncAndGoToOfflineContent(data: MockCanvas = createMockCanvas()) { + val student = data.users.values.first() + val token = data.tokenFor(student).orEmpty() + tokenLogin(data.domain, token, student) + dashboardPage.waitForRender() + leftSideNavigationDrawerPage.clickSettingsMenu() + settingsPage.openOfflineContentPage() + syncSettingsPage.clickWifiOnlySwitch() + syncSettingsPage.clickTurnOff() + Espresso.pressBack() + Espresso.pressBack() + dashboardPage.openGlobalManageOfflineContentPage() + } +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt index 58f45c6131..a7f4775120 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt @@ -82,6 +82,10 @@ class CourseGradesPage : BasePage(R.id.courseGradesPage) { gradeValue.check(matches(matcher)) } + fun assertEmptyView() { + onView(withId(R.id.title) + withText(R.string.noItemsToDisplayShort) + withAncestor(R.id.gradesEmptyView)).assertDisplayed() + } + fun assertAssignmentDisplayed(name: String, gradeString: String) { val siblingMatcher = withId(R.id.title) + withParent(R.id.textContainer) + withText(name) + withAncestor(R.id.courseGradesPage) onView(withId(R.id.points) + hasSibling(siblingMatcher)).scrollTo().assertHasText(gradeString) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index 116ea38c00..78121a4c7e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -260,6 +260,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { .perform(click()); } + //OfflineMethod fun openGlobalManageOfflineContentPage() { Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) onView(withText(containsString("Manage Offline Content"))) @@ -307,7 +308,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { withId(R.id.cardView) + withDescendant(withId(R.id.titleTextView) + withText(courseTitle)) ) - onView(courseOverflowMatcher).scrollTo().click() + waitForView(courseOverflowMatcher).scrollTo().click() waitForView(withId(R.id.title) + withText(menuTitle)).click() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt index dd263c60e8..da39b4cefe 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt @@ -12,21 +12,8 @@ import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel -import com.instructure.espresso.OnViewWithContentDescription -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.assertNotDisplayed -import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.onViewWithId -import com.instructure.espresso.page.onViewWithText -import com.instructure.espresso.page.waitForView -import com.instructure.espresso.page.waitForViewWithId -import com.instructure.espresso.page.withId -import com.instructure.espresso.scrollTo -import com.instructure.espresso.swipeDown -import com.instructure.espresso.swipeUp +import com.instructure.espresso.* +import com.instructure.espresso.page.* import com.instructure.student.R import org.hamcrest.CoreMatchers import org.hamcrest.Matcher @@ -65,6 +52,7 @@ class LeftSideNavigationDrawerPage : BasePage() { ) private fun clickMenu(menuId: Int) { + sleep(1000) //to avoid listview a11y error (content description is missing) waitForView(hamburgerButtonMatcher).click() waitForViewWithId(menuId).scrollTo().click() } @@ -72,7 +60,7 @@ class LeftSideNavigationDrawerPage : BasePage() { fun logout() { onView(hamburgerButtonMatcher).click() logoutButton.scrollTo().click() - onViewWithText(android.R.string.yes).click() + onViewWithText(android.R.string.ok).click() // It can potentially take a long time for the sign-out to take effect, especially on // slow FTL devices. So let's pause for a bit until we see the canvas logo. waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt index 96c5e6ffea..87d65a84ae 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt @@ -17,30 +17,46 @@ package com.instructure.student.ui.pages.offline +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.plus -import com.instructure.espresso.page.waitForView -import com.instructure.espresso.page.withAncestor -import com.instructure.espresso.page.withId -import com.instructure.espresso.page.withText +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.hasCheckedState +import com.instructure.canvas.espresso.withRotation +import com.instructure.espresso.* +import com.instructure.espresso.actions.ForceClick +import com.instructure.espresso.page.* import com.instructure.pandautils.R +import org.hamcrest.CoreMatchers.allOf class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { - private val toolbar by OnViewWithId(R.id.toolbar) + private val syncButton by OnViewWithId(R.id.syncButton) + private val storageInfoContainer by WaitForViewWithId(R.id.storageInfoContainer) //OfflineMethod - fun selectEntireCourseForSync(courseName: String) { - onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(courseName))).click() + fun changeItemSelectionState(itemName: String) { + onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().click() + } + + //OfflineMethod + fun expandCollapseItem(itemName: String) { + onView(withId(R.id.arrow) + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().perform(ForceClick()) + } + + //OfflineMethod + fun expandCollapseFiles() { + expandCollapseItem("Files") } //OfflineMethod fun clickOnSyncButton() { - onView(withId(R.id.syncButton)).click() + syncButton.click() + } + + //OfflineMethod + fun clickOnSyncButtonAndConfirm() { + clickOnSyncButton() confirmSync() } @@ -48,4 +64,101 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { private fun confirmSync() { waitForView(withText("Sync") + withAncestor(R.id.buttonPanel)).click() } + + //OfflineMethod + fun confirmDiscardChanges() { + waitForView(withText("Discard") + withAncestor(R.id.buttonPanel)).click() + } + + //OfflineMethod + fun assertStorageInfoDetails() { + onView(withId(R.id.storageLabel) + withText(R.string.offline_content_storage)).assertDisplayed() + onView(withId(R.id.storageInfo) + containsTextCaseInsensitive("Used")).assertDisplayed() + onView(withId(R.id.progress) + withParent(withId(R.id.storageInfoContainer))).assertDisplayed() + onView(withId(R.id.otherLabel) + withText(R.string.offline_content_other)).assertDisplayed() + onView(withId(R.id.canvasLabel) + withText(R.string.offline_content_canvas_student)).assertDisplayed() + onView(withId(R.id.remainingLabel) + withText(R.string.offline_content_remaining)).assertDisplayed() + } + + //OfflineMethod + fun assertSelectButtonText(selectAll: Boolean) { + if(selectAll) waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).assertDisplayed() + else waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).assertDisplayed() + } + + //OfflineMethod + fun clickOnSelectAllButton() { + waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).click() + } + + //OfflineMethod + fun clickOnDeselectAllButton() { + waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).click() + } + + //OfflineMethod + fun assertCourseCountWithMatcher(expectedCount: Int) { + ConstraintLayoutItemCountAssertionWithMatcher((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))), expectedCount) + } + + //OfflineMethod + fun assertCourseCount(expectedCount: Int) { + onView((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))).check(ConstraintLayoutItemCountAssertion(expectedCount)) + } + + //OfflineMethod + fun assertToolbarTexts(courseName: String) { + onView(withText(courseName) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed() + onView(withText(R.string.offline_content_toolbar_title) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed() + } + + //OfflineMethod + fun assertCheckedStateOfItem(itemName: String, state: Int) { + onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state)).scrollTo().assertDisplayed() + } + + //OfflineMethod + fun waitForItemDisappear(itemName: String) { + onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).check(DoesNotExistAssertion(5)) + } + + //OfflineMethod + fun assertDisplaysNoCourses() { + onView(withText(R.string.offline_content_empty_message)).assertDisplayed() + } + + //OfflineMethod + fun assertDisplaysEmptyCourse() { + onView(withText(R.string.offline_content_empty_course_message)).assertDisplayed() + } + + //OfflineMethod + fun assertDisplaysItemWithExpandedState(title: String, expanded: Boolean) { + onView(withId(R.id.arrow) + + withRotation(if (expanded) 180f else 0f) + + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + + hasSibling(withId(R.id.title) + withText(title)) + ).scrollTo().assertDisplayed() + } + + //OfflineMethod + fun assertItemDisplayed(title: String) { + onView(withId(R.id.title) + withText(title)).scrollTo().assertDisplayed() + } + + //OfflineMethod + fun assertDiscardDialogDisplayed() { + waitForView(withText(R.string.offline_content_discard_dialog_title)).assertDisplayed() + } + + //OfflineMethod + fun assertSyncDialogDisplayed(text: String) { + waitForView(withText(text)).assertDisplayed() + } + + //OfflineMethod + fun assertStorageInfoText(storageInfoText: String) { + onView(withId(R.id.storageInfo) + withText(storageInfoText)).assertDisplayed() + } } + diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index 3521f438d0..674981de32 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -48,6 +48,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import com.airbnb.lottie.LottieAnimationView import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.canvasapi2.CanvasRestAdapter import com.instructure.canvasapi2.managers.GroupManager import com.instructure.canvasapi2.managers.UserManager @@ -67,6 +68,7 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.features.help.HelpDialogFragment import com.instructure.pandautils.features.inbox.list.InboxFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment +import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.models.PushNotification @@ -150,6 +152,12 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. @Inject lateinit var offlineDatabase: OfflineDatabase + @Inject + lateinit var offlineSyncHelper: OfflineSyncHelper + + @Inject + lateinit var firebaseCrashlytics: FirebaseCrashlytics + private var routeJob: WeaveJob? = null private var debounceJob: Job? = null private var drawerItemSelectedJob: Job? = null @@ -205,14 +213,14 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. R.id.navigationDrawerItem_logout -> { AlertDialog.Builder(this@NavigationActivity) .setTitle(R.string.logout_warning) - .setPositiveButton(android.R.string.yes) { _, _ -> + .setPositiveButton(android.R.string.ok) { _, _ -> StudentLogoutTask( LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior, databaseProvider = databaseProvider ).execute() } - .setNegativeButton(android.R.string.no, null) + .setNegativeButton(android.R.string.cancel, null) .create() .show() } @@ -316,6 +324,12 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. setOfflineState(!isOnline) handleTokenCheck(isOnline) } + + lifecycleScope.tryLaunch { + offlineSyncHelper.scheduleWorkAfterLogin() + } catch { + firebaseCrashlytics.recordException(it) + } } private fun handleTokenCheck(online: Boolean?) { diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/FileSearchModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/FileSearchModule.kt new file mode 100644 index 0000000000..040dd05b97 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/FileSearchModule.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 - 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.student.di.feature + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.files.search.FileSearchLocalDataSource +import com.instructure.student.features.files.search.FileSearchNetworkDataSource +import com.instructure.student.features.files.search.FileSearchRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class FileSearchModule { + + @Provides + fun provideFileSearchLocalDataSource( + fileFolderDao: FileFolderDao, + localFileDao: LocalFileDao + ): FileSearchLocalDataSource { + return FileSearchLocalDataSource(fileFolderDao, localFileDao) + } + + @Provides + fun provideFileSearchNetworkDataSource( + fileFolderApi: FileFolderAPI.FilesFoldersInterface + ): FileSearchNetworkDataSource { + return FileSearchNetworkDataSource(fileFolderApi) + } + + @Provides + fun provideFileSearchRepository( + fileSearchLocalDataSource: FileSearchLocalDataSource, + fileSearchNetworkDataSource: FileSearchNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): FileSearchRepository { + return FileSearchRepository( + fileSearchLocalDataSource, + fileSearchNetworkDataSource, + networkStateProvider, + featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt index 75279fe0b8..e76d4dbabe 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt @@ -61,10 +61,7 @@ import com.instructure.student.events.DiscussionUpdatedEvent import com.instructure.student.events.ModuleUpdatedEvent import com.instructure.student.events.post import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment -import com.instructure.student.fragment.DiscussionsReplyFragment -import com.instructure.student.fragment.DiscussionsUpdateFragment -import com.instructure.student.fragment.InternalWebviewFragment -import com.instructure.student.fragment.ParentFragment +import com.instructure.student.fragment.* import com.instructure.student.router.RouteMatcher import com.instructure.student.util.Const import dagger.hilt.android.AndroidEntryPoint @@ -272,10 +269,10 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { if (repository.isOnline()) { val builder = AlertDialog.Builder(requireContext()) builder.setMessage(R.string.utils_discussionsDeleteWarning) - builder.setPositiveButton(android.R.string.yes) { _, _ -> + builder.setPositiveButton(android.R.string.ok) { _, _ -> deleteDiscussionEntry(discussionEntryId) } - builder.setNegativeButton(android.R.string.no) { _, _ -> } + builder.setNegativeButton(android.R.string.cancel) { _, _ -> } val dialog = builder.create() dialog.setOnShowListener { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ThemePrefs.textButtonColor) @@ -743,7 +740,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { discussionTopicHeaderWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), discussionTopicHeader.message, { if (view != null) loadHTMLTopic(it, discussionTopicHeader.title) - }) + }, onLtiButtonPressed = { LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) }) attachmentIcon.setVisible(discussionTopicHeader.attachments.isNotEmpty()) attachmentIcon.onClick { @@ -762,9 +759,9 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { setupRepliesWebView() - discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, { - discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), html, "text/html", "UTF-8", null) - }) + discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, { formattedHtml -> + discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), formattedHtml, "text/html", "UTF-8", null) + }, onLtiButtonPressed = { LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) }) swipeRefreshLayout.isRefreshing = false discussionTopicRepliesTitle.setVisible(discussionTopicHeader.shouldShowReplies) diff --git a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt index 6908031cde..4c4bfbbfe4 100644 --- a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt @@ -119,17 +119,7 @@ class FileDetailsFragment : ParentFragment() { private fun setupClickListeners() { binding.openButton.setOnClickListener { file?.let { fileFolder -> - when { - fileFolder.isLocalFile -> { - openLocalMedia( - fileFolder.contentType, - fileFolder.url, - fileFolder.displayName, - canvasContext - ) - } - else -> openMedia(fileFolder.contentType, fileFolder.url, fileFolder.displayName, canvasContext) - } + openMedia(fileFolder.contentType, fileFolder.url, fileFolder.displayName, canvasContext, fileFolder.isLocalFile) markAsRead() } } diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt index ace8cd4d9f..553bb47c90 100644 --- a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt @@ -222,13 +222,9 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent recordFilePreviewEvent(item) openHtmlUrl(item) } - item.isLocalFile -> { - recordFilePreviewEvent(item) - openLocalMedia(item.contentType, item.url, item.displayName, canvasContext) - } else -> { recordFilePreviewEvent(item) - openMedia(item.contentType, item.url, item.displayName, canvasContext) + openMedia(item.contentType, item.url, item.displayName, canvasContext, localFile = item.isLocalFile) } } } @@ -378,11 +374,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent when (menuItem.itemId) { R.id.openAlternate -> { recordFilePreviewEvent(item) - if (fileListRepository.isOnline()) { - openMedia(item.contentType, item.url, item.displayName, true, canvasContext) - } else { - openLocalMedia(item.contentType, item.url, item.displayName, canvasContext, true) - } + openMedia(item.contentType, item.url, item.displayName, canvasContext, localFile = !fileListRepository.isOnline(), useOutsideApps = true) } R.id.download -> downloadItem(item) R.id.rename -> renameItem(item) diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt index d0b4a54768..35cc826a59 100644 --- a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt @@ -26,6 +26,8 @@ import com.instructure.canvasapi2.utils.weave.WeaveJob import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.pandautils.utils.textAndIconColor import com.instructure.student.adapter.BaseListRecyclerAdapter import com.instructure.student.features.files.list.FileFolderCallback @@ -34,6 +36,7 @@ import com.instructure.student.holders.FileViewHolder class FileSearchAdapter( context: Context, private val canvasContext: CanvasContext, + private val fileSearchRepository: FileSearchRepository, private val viewCallback: FileSearchView ) : BaseListRecyclerAdapter(context, FileFolder::class.java) { @@ -83,9 +86,7 @@ class FileSearchAdapter( private fun performSearch() { apiCall = tryWeave { viewCallback.onRefreshStarted() - val files = awaitApi> { - FileFolderManager.searchFiles(searchQuery, canvasContext, true, it) - } + val files = fileSearchRepository.searchFiles(canvasContext, searchQuery) clear() addAll(files) viewCallback.onRefreshFinished() diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchDataSource.kt new file mode 100644 index 0000000000..263fba8b2c --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchDataSource.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 - 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.student.features.files.search + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder + +interface FileSearchDataSource { + + suspend fun searchFiles(canvasContext: CanvasContext, searchQuery: String): List +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchFragment.kt index 39342f2b74..3cf60d7cf3 100644 --- a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchFragment.kt @@ -31,24 +31,31 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_FILE_SEARCH import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.room.offline.daos.FileFolderDao import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.databinding.FragmentFileSearchBinding import com.instructure.student.fragment.ParentFragment +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import com.instructure.pandautils.utils.ColorUtils as PandaColorUtils @ScreenView(SCREEN_VIEW_FILE_SEARCH) +@AndroidEntryPoint class FileSearchFragment : ParentFragment(), FileSearchView { private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) private val binding by viewBinding(FragmentFileSearchBinding::bind) + @Inject + lateinit var fileSearchRepository: FileSearchRepository + private fun makePageViewUrl() = if (canvasContext.type == CanvasContext.Type.USER) "${ApiPrefs.fullDomain}/files" else "${ApiPrefs.fullDomain}/${canvasContext.contextId.replace("_", "s/")}/files" - private val searchAdapter by lazy { FileSearchAdapter(requireContext(), canvasContext, this) } + private val searchAdapter by lazy { FileSearchAdapter(requireContext(), canvasContext, fileSearchRepository, this) } override fun title() = "" override fun applyTheme() = Unit @@ -137,7 +144,7 @@ class FileSearchFragment : ParentFragment(), FileSearchView { override fun fileClicked(file: FileFolder) { PageViewUtils.saveSingleEvent("FilePreview", "${makePageViewUrl()}?preview=${file.id}") - openMedia(file.contentType, file.url, file.displayName, canvasContext) + openMedia(file.contentType, file.url, file.displayName, canvasContext, file.isLocalFile) } override fun onMediaLoadingStarted() { diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchLocalDataSource.kt new file mode 100644 index 0000000000..cb054d231e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchLocalDataSource.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - 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.student.features.files.search + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao + +class FileSearchLocalDataSource( + private val fileFolderDao: FileFolderDao, + private val localFileDao: LocalFileDao +) : FileSearchDataSource { + override suspend fun searchFiles(canvasContext: CanvasContext, searchQuery: String): List { + val files = fileFolderDao.searchCourseFiles(canvasContext.id, searchQuery).map { it.toApiModel() } + val fileIds = files.map { it.id } + val localFileMap = localFileDao.findByIds(fileIds).associate { it.id to it.path } + + return files.map { it.copy(url = localFileMap[it.id], thumbnailUrl = null) } + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchNetworkDataSource.kt new file mode 100644 index 0000000000..2f8a42cb24 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchNetworkDataSource.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 - 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.student.features.files.search + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.depaginate + +class FileSearchNetworkDataSource(private val fileFolderApi: FileFolderAPI.FilesFoldersInterface) : FileSearchDataSource { + override suspend fun searchFiles(canvasContext: CanvasContext, searchQuery: String): List { + val params = RestParams(isForceReadFromNetwork = true, usePerPageQueryParam = true) + return fileFolderApi.searchFiles(canvasContext.toAPIString().substring(1), searchQuery, params) + .depaginate { nextUrl -> fileFolderApi.getNextPageFileFoldersList(nextUrl, params) } + .dataOrNull.orEmpty() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchRepository.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchRepository.kt new file mode 100644 index 0000000000..b0a1fec013 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchRepository.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 - 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.student.features.files.search + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider + +class FileSearchRepository( + fileSearchLocalDataSource: FileSearchLocalDataSource, + fileSearchNetworkDataSource: FileSearchNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository( + fileSearchLocalDataSource, + fileSearchNetworkDataSource, + networkStateProvider, + featureFlagProvider +) { + suspend fun searchFiles( + canvasContext: CanvasContext, + searchQuery: String + ): List { + return dataSource().searchFiles(canvasContext, searchQuery) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt index be869584af..872a8233b5 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt @@ -223,7 +223,7 @@ class BookmarksFragment : ParentFragment() { val builder = AlertDialog.Builder(requireContext()) builder.setTitle(R.string.bookmarkDelete) builder.setMessage(bookmark.name) - builder.setPositiveButton(android.R.string.yes) { _, _ -> + builder.setPositiveButton(android.R.string.ok) { _, _ -> BookmarkManager.deleteBookmark(bookmark.id, object : StatusCallback() { override fun onResponse(response: retrofit2.Response, linkHeaders: LinkHeaders, type: ApiType) { if (isAdded && response.code() == 200) { @@ -239,7 +239,7 @@ class BookmarksFragment : ParentFragment() { }) } - builder.setNegativeButton(android.R.string.no, null) + builder.setNegativeButton(android.R.string.cancel, null) val dialog = builder.create() dialog.show() } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt index 2f52aadbb7..4cbdbeb85e 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt @@ -371,18 +371,16 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions, Navigati return recyclerView } - fun openMedia(mime: String?, url: String?, filename: String?, canvasContext: CanvasContext) { + fun openMedia(mime: String?, url: String?, filename: String?, canvasContext: CanvasContext, localFile: Boolean = false, useOutsideApps: Boolean = false) { val owner = activity ?: return - onMainThread { - openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(canvasContext, mime, url, filename) - LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID) + + openMediaBundle = if (localFile) { + OpenMediaAsyncTaskLoader.createLocalBundle(canvasContext, mime, url, filename, useOutsideApps) + } else { + OpenMediaAsyncTaskLoader.createBundle(canvasContext, mime, url, filename, useOutsideApps) } - } - fun openLocalMedia(mime: String?, path: String?, filename: String?, canvasContext: CanvasContext, useOutsideApps: Boolean = false) { - val owner = activity ?: return onMainThread { - openMediaBundle = OpenMediaAsyncTaskLoader.createLocalBundle(canvasContext, mime, path, filename, useOutsideApps) LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID) } } @@ -395,14 +393,6 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions, Navigati } } - fun openMedia(mime: String?, url: String?, filename: String?, useOutsideApps: Boolean, canvasContext: CanvasContext) { - val owner = activity ?: return - onMainThread { - openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(canvasContext, mime, url, filename, useOutsideApps) - LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID) - } - } - private fun downloadFileToDownloadDir(url: String): File? { // We should have the file cached locally at this point; We'll just move it to the user's Downloads folder diff --git a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt index 8446c3f04b..ec829ffbc1 100644 --- a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt +++ b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt @@ -19,11 +19,14 @@ package com.instructure.student.tasks import android.content.Context import android.content.Intent import android.net.Uri +import androidx.work.WorkManager import com.google.firebase.messaging.FirebaseMessaging import com.heapanalytics.android.Heap import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.canvasapi2.utils.tryOrNull import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.features.offline.sync.CourseSyncWorker +import com.instructure.pandautils.features.offline.sync.OfflineSyncWorker import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.pandautils.typeface.TypefaceBehavior import com.instructure.student.activity.LoginActivity @@ -72,4 +75,12 @@ class StudentLogoutTask( databaseProvider?.clearDatabase(it) } } + + override fun stopOfflineSync() { + val workManager = WorkManager.getInstance(ContextKeeper.appContext) + workManager.apply { + cancelAllWorkByTag(CourseSyncWorker.TAG) + cancelAllWorkByTag(OfflineSyncWorker.TAG) + } + } } diff --git a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml index 418ccaff37..7fac5f8220 100644 --- a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml +++ b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml @@ -110,7 +110,9 @@ android:id="@+id/courseName" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/course_name_gradient" + android:background="@color/translucentBlack" + android:ellipsize="end" + android:maxLines="2" android:paddingStart="16dp" android:paddingTop="10dp" android:paddingEnd="16dp" diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt new file mode 100644 index 0000000000..c9a46e177a --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 - 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.student.features.file.search + +import android.webkit.URLUtil +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.entities.FileFolderEntity +import com.instructure.pandautils.room.offline.entities.LocalFileEntity +import com.instructure.student.features.files.search.FileSearchLocalDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@ExperimentalCoroutinesApi +class FileSearchLocalDataSourceTest { + + private val fileFolderDao: FileFolderDao = mockk(relaxed = true) + private val localFileDao: LocalFileDao = mockk(relaxed = true) + + private val fileSearchLocalDataSource = FileSearchLocalDataSource(fileFolderDao, localFileDao) + + @Before + fun setup() { + mockkStatic(URLUtil::class) + every { URLUtil.isNetworkUrl(any()) } returns true + } + + @After + fun teardown() { + unmockkAll() + } + + + @Test + fun `File Search replaces url with path`() = runTest { + val files = listOf( + FileFolder(id = 1, name = "File 1", url = "url_1", thumbnailUrl = "thumbnail_url_1"), + FileFolder(id = 2, name = "File 2", url = "url_2", thumbnailUrl = "thumbnail_url_2"), + FileFolder(id = 3, name = "File 3", url = "url_3", thumbnailUrl = "thumbnail_url_3") + ) + + val localFiles = listOf( + LocalFileEntity(id = 1, courseId = 1, createdDate = Date(), path = "path_1"), + LocalFileEntity(id = 2, courseId = 1, createdDate = Date(), path = "path_2"), + LocalFileEntity(id = 3, courseId = 1, createdDate = Date(), path = "path_3") + ) + + coEvery { localFileDao.findByIds(any()) } returns localFiles + coEvery { fileFolderDao.searchCourseFiles(any(), any()) } returns files.map { FileFolderEntity(it) } + + val expected = files.map { it.copy(url = "path_${it.id}", thumbnailUrl = null) } + val result = fileSearchLocalDataSource.searchFiles(Course(1L), "") + + coVerify { + localFileDao.findByIds(listOf(1, 2, 3)) + fileFolderDao.searchCourseFiles(1L, "") + } + + TestCase.assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt new file mode 100644 index 0000000000..a8e68b0870 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 - 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.student.features.file.search + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.files.search.FileSearchNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class FileSearchNetworkDataSourceTest { + + private val fileFolderApi: FileFolderAPI.FilesFoldersInterface = mockk(relaxed = true) + + private val fileSearchNetworkDataSource = FileSearchNetworkDataSource(fileFolderApi) + + @Test + fun `searchFiles() calls api and returns data from api`() = runTest { + coEvery { fileFolderApi.searchFiles(any(), any(), any()) } returns DataResult.Success(listOf(FileFolder(id = 1, name = "File"))) + + val result = fileSearchNetworkDataSource.searchFiles(Course(1), "file") + + coVerify { fileFolderApi.searchFiles("courses/1", "file",any()) } + assertEquals(1, result.size) + assertEquals("File", result[0].name) + } + + @Test + fun `searchFiles() returns empty list for failed result`() = runTest { + coEvery { fileFolderApi.searchFiles(any(), any(), any()) } returns DataResult.Fail() + + val result = fileSearchNetworkDataSource.searchFiles(Course(1), "file") + + coVerify { fileFolderApi.searchFiles("courses/1", "file",any()) } + assertEquals(0, result.size) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt new file mode 100644 index 0000000000..a6bed1cf59 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 - 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.student.features.file.search + +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.files.search.FileSearchLocalDataSource +import com.instructure.student.features.files.search.FileSearchNetworkDataSource +import com.instructure.student.features.files.search.FileSearchRepository +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class FileSearchRepositoryTest { + + private val fileSearchLocalDataSource: FileSearchLocalDataSource = mockk(relaxed = true) + private val fileSearchNetworkDataSource: FileSearchNetworkDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val fileSearchRepository = FileSearchRepository( + fileSearchLocalDataSource, + fileSearchNetworkDataSource, + networkStateProvider, + featureFlagProvider + ) + + @Before + fun setup() { + coEvery { featureFlagProvider.offlineEnabled() } returns true + coEvery { networkStateProvider.isOnline() } returns true + } + + @Test + fun `use localDataSource when network is offline`() = runTest { + coEvery { networkStateProvider.isOnline() } returns false + + assertTrue(fileSearchRepository.dataSource() is FileSearchLocalDataSource) + } + + @Test + fun `use networkDataSource when network is online`() = runTest { + coEvery { networkStateProvider.isOnline() } returns true + + assertTrue(fileSearchRepository.dataSource() is FileSearchNetworkDataSource) + } +} \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt index 2b64f29b24..19191bce82 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt @@ -9,15 +9,11 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.dataseeding.model.CanvasUserApiModel -import com.instructure.espresso.OnViewWithContentDescription -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.click +import com.instructure.espresso.* import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.onViewWithId import com.instructure.espresso.page.onViewWithText -import com.instructure.espresso.scrollTo import com.instructure.teacher.R import org.hamcrest.CoreMatchers import org.hamcrest.Matcher @@ -61,7 +57,7 @@ class LeftSideNavigationDrawerPage: BasePage() { fun logout() { onView(hamburgerButtonMatcher).click() logoutButton.scrollTo().click() - onViewWithText(android.R.string.yes).click() + onViewWithText(android.R.string.ok).click() // It can potentially take a long time for the sign-out to take effect, especially on // slow FTL devices. So let's pause for a bit until we see the canvas logo. waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(matches( diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt index 9bd92c4cbe..a00a2671b8 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt @@ -322,8 +322,8 @@ class InitActivity : BasePresenterActivity { AlertDialog.Builder(this@InitActivity) .setTitle(R.string.logout_warning) - .setPositiveButton(android.R.string.yes) { _, _ -> TeacherLogoutTask(LogoutTask.Type.LOGOUT).execute() } - .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.ok) { _, _ -> TeacherLogoutTask(LogoutTask.Type.LOGOUT).execute() } + .setNegativeButton(android.R.string.cancel, null) .create() .show() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt index ddffe62c13..8630ff64f3 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt @@ -57,17 +57,7 @@ import com.instructure.teacher.dialog.NoInternetConnectionDialog import com.instructure.teacher.events.* import com.instructure.teacher.events.DiscussionEntryEvent import com.instructure.teacher.factory.DiscussionsDetailsPresenterFactory -import com.instructure.teacher.fragments.AssignmentSubmissionListFragment -import com.instructure.teacher.fragments.CreateDiscussionFragment -import com.instructure.teacher.fragments.CreateOrEditAnnouncementFragment -import com.instructure.teacher.fragments.DiscussionBottomSheetChoice -import com.instructure.teacher.fragments.DiscussionBottomSheetMenuFragment -import com.instructure.teacher.fragments.DiscussionsReplyFragment -import com.instructure.teacher.fragments.DiscussionsUpdateFragment -import com.instructure.teacher.fragments.DueDatesFragment -import com.instructure.teacher.fragments.FullscreenInternalWebViewFragment -import com.instructure.teacher.fragments.InternalWebViewFragment -import com.instructure.teacher.fragments.LtiLaunchFragment +import com.instructure.teacher.fragments.* import com.instructure.teacher.presenters.AssignmentSubmissionListPresenter import com.instructure.teacher.presenters.DiscussionsDetailsPresenter import com.instructure.teacher.router.RouteMatcher @@ -271,8 +261,8 @@ class DiscussionsDetailsFragment : BasePresenterFragment< discussionRepliesWebViewWrapper.setInvisible() - repliesLoadHtmlJob = discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, { - discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), html, "text/html", "utf-8", null) + repliesLoadHtmlJob = discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, {formattedHtml -> + discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), formattedHtml, "text/html", "utf-8", null) }) { LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) } @@ -690,10 +680,10 @@ class DiscussionsDetailsFragment : BasePresenterFragment< if (APIHelper.hasNetworkConnection()) { val builder = AlertDialog.Builder(requireContext()) builder.setMessage(R.string.discussions_delete_warning) - builder.setPositiveButton(android.R.string.yes) { _, _ -> + builder.setPositiveButton(android.R.string.ok) { _, _ -> presenter.deleteDiscussionEntry(id) } - builder.setNegativeButton(android.R.string.no) { _, _ -> } + builder.setNegativeButton(android.R.string.cancel) { _, _ -> } val dialog = builder.create() dialog.setOnShowListener { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ThemePrefs.textButtonColor) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt index 2d7042273a..52679d13af 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt @@ -23,10 +23,7 @@ import android.os.Bundle import android.view.Menu import android.view.View import android.view.animation.AnimationUtils -import android.webkit.CookieManager -import android.webkit.WebChromeClient -import android.webkit.WebView -import android.webkit.WebViewClient +import android.webkit.* import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.apis.AttendanceAPI @@ -206,8 +203,11 @@ class AttendanceListFragment : BaseSyncFragment< // Tried this headless without adding to the root view but it ended up loading faster when the view exists in the view group. CookieManager.getInstance().acceptCookie() CookieManager.getInstance().acceptThirdPartyCookies(webView) - webView.settings.javaScriptEnabled = true - webView.settings.useWideViewPort = true + webView.settings.apply { + javaScriptEnabled = true + useWideViewPort = true + domStorageEnabled = true + } webView.webChromeClient = WebChromeClient() webView.webViewClient = object: WebViewClient(){ override fun onPageFinished(view: WebView?, url: String?) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt index 16c9963a74..6b0b9e2548 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt @@ -51,8 +51,8 @@ open class InternalWebViewFragment : BaseFragment() { var title: String by StringArg() var darkToolbar: Boolean by BooleanArg() private var shouldAuthenticate: Boolean by BooleanArg(key = AUTHENTICATE) + var shouldRouteInternally: Boolean by BooleanArg(key = SHOULD_ROUTE_INTERNALLY, default = true) - private var shouldRouteInternally = true private var shouldLoadUrl = true private var mSessionAuthJob: Job? = null private var shouldCloseFragment = false @@ -62,10 +62,6 @@ open class InternalWebViewFragment : BaseFragment() { // Used for external urls that reject the candroid user agent string var originalUserAgentString: String = "" - protected fun setShouldRouteInternally(shouldRouteInternally: Boolean) { - this.shouldRouteInternally = shouldRouteInternally - } - override fun onPause() { super.onPause() binding.canvasWebView.onPause() @@ -242,6 +238,7 @@ open class InternalWebViewFragment : BaseFragment() { const val HTML = "html" const val DARK_TOOLBAR = "darkToolbar" const val AUTHENTICATE = "authenticate" + private const val SHOULD_ROUTE_INTERNALLY = "shouldRouteInternally" fun newInstance(url: String) = InternalWebViewFragment().apply { this.url = url @@ -263,6 +260,8 @@ open class InternalWebViewFragment : BaseFragment() { title = args.getString(TITLE)!! html = args.getString(HTML) ?: "" darkToolbar = args.getBoolean(DARK_TOOLBAR) + shouldAuthenticate = args.getBoolean(AUTHENTICATE) + shouldRouteInternally = args.getBoolean(SHOULD_ROUTE_INTERNALLY) } @JvmOverloads @@ -275,13 +274,14 @@ open class InternalWebViewFragment : BaseFragment() { return args } - fun makeBundle(url: String, title: String, darkToolbar: Boolean = false, html: String = "", shouldAuthenticate: Boolean): Bundle { + fun makeBundle(url: String, title: String, darkToolbar: Boolean = false, html: String = "", shouldRouteInternally: Boolean = true, shouldAuthenticate: Boolean): Bundle { val args = Bundle() args.putString(URL, url) args.putString(TITLE, title) args.putString(HTML, html) args.putBoolean(DARK_TOOLBAR, darkToolbar) args.putBoolean(AUTHENTICATE, shouldAuthenticate) + args.putBoolean(SHOULD_ROUTE_INTERNALLY, shouldRouteInternally) return args } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SimpleWebViewFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SimpleWebViewFragment.kt index 8f480ff4ed..697e80b231 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SimpleWebViewFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SimpleWebViewFragment.kt @@ -35,7 +35,7 @@ class SimpleWebViewFragment : InternalWebViewFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { setShouldLoadUrl(false) setShouldAuthenticateUponLoad(true) - setShouldRouteInternally(false) + shouldRouteInternally = false binding.canvasWebView.setInitialScale(100) super.onActivityCreated(savedInstanceState) binding.canvasWebView.enableAlgorithmicDarkening() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt index f18fca18f1..ad31cc278e 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt @@ -55,7 +55,7 @@ class SpeedGraderLtiSubmissionFragment : Fragment() { private fun setupViews() { ViewStyler.themeButton(binding.viewLtiButton) binding.viewLtiButton.onClick { - val args = InternalWebViewFragment.makeBundle(mUrl, getString(R.string.canvasAPI_externalTool)) + val args = InternalWebViewFragment.makeBundle(mUrl, getString(R.string.canvasAPI_externalTool), shouldAuthenticate = true, shouldRouteInternally = false) RouteMatcher.route(requireActivity(), Route(InternalWebViewFragment::class.java, mCanvasContext, args)) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderQuizWebViewFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderQuizWebViewFragment.kt index c9d49bcc3d..593926cdc2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderQuizWebViewFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderQuizWebViewFragment.kt @@ -56,7 +56,7 @@ class SpeedGraderQuizWebViewFragment : InternalWebViewFragment() { (requireContext() as Activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED setShouldAuthenticateUponLoad(true) - setShouldRouteInternally(false) + shouldRouteInternally = false setShouldLoadUrl(false) canvasWebView.setInitialScale(100) super.onActivityCreated(savedInstanceState) diff --git a/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml b/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml index 8a3d12393e..21f662930b 100644 --- a/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml +++ b/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml @@ -65,7 +65,7 @@ android:layout_below="@+id/canvasWordmarkStudent" android:layout_toEndOf="@id/studentCanvasLogo" android:text="@string/appUserTypeStudent" - android:textColor="@color/login_studentAppTheme"/> + android:textColor="@color/textDarkest"/> @@ -93,7 +93,7 @@ android:layout_below="@+id/canvasWordmarkParent" android:layout_toEndOf="@id/parentCanvasLogo" android:text="@string/appUserTypeParent" - android:textColor="@color/login_parentAppTheme"/> + android:textColor="@color/textDarkest"/> diff --git a/apps/teacher/src/main/res/layout/navigation_drawer.xml b/apps/teacher/src/main/res/layout/navigation_drawer.xml index 57de755a08..de3f459f15 100644 --- a/apps/teacher/src/main/res/layout/navigation_drawer.xml +++ b/apps/teacher/src/main/res/layout/navigation_drawer.xml @@ -119,7 +119,7 @@ + app:srcCompat="@drawable/ic_navigation_studio" /> . + --> + + + + Pengaturan + Minggu + Kotak Masuk + Detail + Pertanyaan + Penyerahan + Tugas + Profil + Orang + Kuis + Diskusi + Pengumuman + Kehadiran + Halaman + Harus Dilakukan + Logo Sekolah + Cari di Panduan Canvas + Panduan Canvas + Ikon untuk Panduan Canvas + Jawaban untuk pertanyaan yang sering ditanyakan + Laporkan masalah + Ikon untuk Laporkan Masalah + Jika aplikasi bermasalah, beri tahu kami + Minta Fitur + Ikon untuk Minta Fitur + Beri tahu kami tentang ide Anda untuk meningkatkan aplikasi + Bagikan Cinta Anda untuk Aplikasi + Beri tahu kami bagian favorit Anda dari aplikasi + Penyamaran + Masalah dengan Teacher App [Android] + Tidak ada item + Nilai aplikasi + Ikon untuk Nilai aplikasi + Logout + Ganti Pengguna + Sedang Dibuat + Acara + Lokasi + Tanggal Acara + Kursus + Profil + Bagikan + Bagikan Dengan… + + + Buka laci navigasi + Tutup laci navigasi + + + (tanpa subjek) + Buat pesan + Pesan Baru + Kursus + Buat Pesan + Kirim + Lampiran + Lampiran + Fungsionalitas tidak tersedia saat offline + Pesan tidak boleh kosong! + Pesan ini saat ini tidak memiliki penerima. + Kursus: + Balas + Teruskan Pesan + Kepada + Kursus + Subjek + + Kirim + Edit + + + Ide untuk Teacher App [Android] + Informasi berikut akan membantu kami memahami ide Anda lebih baik: + Kirim Email… + + Merah + Hot Pink + Lavender + Violet + Ungu + Slate + Biru + Sian + Hijau + Chartreuse + Kuning + Emas + Oranye + Merah Muda + Abu-Abu + + Ikon Lampiran + Ikon Bintang + Monolog + + Judul Topik + Tanggal Posting + Pengumuman + Pengumuman Baru + Deskripsi harus ada + Detail Pengumuman + + Tanpa Judul + Pengumuman Dibuat! + Tidak dapat membuat pengumuman. Silakan periksa sambungan internet Anda dan coba lagi. + Tunda Posting + + Diskusi + + Minggu + Senin + Selasa + Rabu + Kamis + Jumat + Sabtu + + Tambah Lampiran + Ikon file + Berikutnya + Unggah Ke Canvas + File Saya + File Kursus + Unggah Ke + Lampiran Pesan + dengan + Lampirkan file + Pilih file atau foto + Mengunggah file… + (Tanpa Lampiran) + Membuat Pengumuman… + + Salin alamat email + Semua Periode Penilaian + Tidak ada penyerahan + Semua penyerahan dinilai + ikon + + %d penyerahan membutuhkan penilaian + %d penyerahan membutuhkan penilaian + + + + Ada %d penerima penetapan tanpa nilai. + Ada %d penerima penetapan tanpa nilai. + + + Tidak ada pesan + Tanpa Pesan Dibintangi + Ketuk \"+\" untuk membuat percakapan baru. + Bintangi pesan dengan mengetuk bintang pada pesan. + + Balas + Balas Semua + Maju + Bintang + Arsipkan + Hapus + Pesan + Keluarkan dari arsip + Opsi Pesan + Tandai Belum Dibaca + Anda yakin mau menghapus salinan Anda dari pesan ini? Tindakan ini tidak bisa diurungkan + Anda yakin mau menghapus salinan Anda dari percakapan ini? Tindakan ini tidak bisa diurungkan + Tidak dapat melakukan tindakan ini. Silakan periksa sambungan internet Anda dan coba lagi. + Pesan diarsipkan + Pesan dikeluarkan dari arsip + Pesan dihapus + Pilih Penerima + Seluruh grup dipilih + Tidak ada pengguna dalam grup + + Diterbitkan + Belum Diterbitkan + Tanda centang diterbitkan + poin + poin + Tidak Ada Batas Waktu + Batas Waktu + + Lihat Balasan + Lihat Diskusi + + Edit + Simpan + Berhasil memperbarui tugas. + Berhasil memperbarui kuis. + Terjadi kesalahan saat memperbarui tugas. Cobalah lagi. + Terjadi kesalahan saat menghapus tugas. Cobalah lagi. + Tidak dapat menghapus terbitan jika. ada penyerahan siswa + Pesan + + + %d Pesan + + %d Pesan + + + %d penyerahan membutuhkan penilaian + %d penyerahan membutuhkan penilaian + + + Anda yakin mau menghapus tugas ini? + + 99+ + Kirim Pesan ke Siswa + + Kirim pesan ke siswa ini + Tidak Ada + Terlambat + Diserahkan + Belum diserahkan + Dibolehkan + Login terakhir: %s + + Hidupkan Penilaian Anonim + Matikan Penilaian Anonim + Penilaian Anonim + Siswa + + + Fitur Segera Hadir + + + Semua Penyerahan + Penyerahan Terlambat + Penyerahan Tidak Ada + Nilai Tinggi + Nilai Rendah + + + + Menurut Nama + Nilai Tinggi ke Rendah + Nilai Rendah ke Tinggi + + + Tab Kursus + Tab Kotak Masuk + Tab Profil + Tutup + Silakan pilih kursus + Balasan Diskusi + Balasan Pengumuman + Tambah teks untuk mengirim pesan + Pesan ini tidak dapat dikirim. Ketuk untuk mencoba lagi. + Pesan berhasil dikirim. + Terjadi kesalahan yang tidak terduga. + Kursus ini tidak memiliki tugas yang mengizinkan unggahan file. + Jenis file yang dipilih tidak diizinkan. + Ekstensi yang diizinkan:  + File berhasil diunggah. + Terjadi kesalahan saat mengunggah file Anda. Silakan coba lagi. + Mengunggah file… + Perangkat Anda tidak memiliki aplikasi apa pun yang terinstal untuk menangani file ini. + Serahkan + Terjadi kesalahan saat mengambil foto. Silakan coba lagi. + Memuat File… + Anda belum memilih file apa pun. + Anda tidak dapat mengunggah file ke tugas yang dipilih. + Satu atau lebih file memiliki ekstensi yang tidak diizinkan + + Kotak Masuk + Belum dibaca + Membintangi + Terkirim + Diarsipkan + Semua + Mengunggah %d dari %d + Halaman tidak ditemukan + Tanpa Koneksi + Anda tidak berwenang untuk mengakses ini. Anda tidak memiliki izin atau Anda belum memiliki akses (contohnya, kursus Anda belum dimulai) + Terjadi kesalahan server. + Terjadi kesalahan saat memuat file Anda. Silakan periksa sambungan internet Anda dan coba lagi. + File berhasil diserahkan. + Silakan periksa sambungan data Anda dan coba lagi. + Anda yakin mau logout? + Ya + Tidak + Sedang mengirim… + Batal + Ok + Unggah + Pengaturan + Edit Kursus + Mulai Menganotasi + Kosongkan Tampilan SpeedGrader, Butuh Desainer + + + Pengaturan Kursus + Nama Kursus + Atur \'\'Home\'\' ke... + Stream Aktivitas Kursus + Halaman Depan Halaman + Halaman Depan + Modul Kursus + Daftar Tugas + Silabus + + + Tanggal Batas Berganda + Semua Orang + Semua orang lain + Siswa Tidak Dikenal + Ditutup + -- + Batas waktu %1s pada %2s + Posting terakhir%s + Jenis Penyerahan + Detail tanggal batas lengkap + Ketersediaan: + Tersedia untuk: + Tersedia mulai: + Untuk: + Batas waktu: + Tersedia hingga + Tersedia dari + Untuk + Batas + Detail Tugas + Tidak Ada Konten + Penyerahan + Bantu siswa Anda dengan tugas ini dengan menambah instruktur. + Bantu siswa Anda dengan kuis ini dengan menambah instruktur. + Tanggal Batas + Edit Tugas + Edit Kuis + Judul + Deskripsi + Total Nilai + Kelas + Masukkan Nilai + Override + Dihitung berdasarkan rubrik + Kustomisasikan Nilai + Dari %1$s + IPK + Nilai Dalam Huruf + Bolehkan siswa + Bolehkan grup + Lengkap + Tidak lengkap + Tidak Dinilai + Dibolehkan + %s %s + %1$s dari %2$s + Terbitkan + Kesalahan terjadi saat mencoba menyimpan tugas. Cobalah lagi. + Kesalahan terjadi saat mencoba menyimpan kuis. Cobalah lagi. + Nama tugas harus ditetapkan. + Judul pengumuman harus ditetapkan. + Judul kuis harus ditetapkan. + Poin tugas harus ditetapkan. + Poin yang memungkinkan harus angka + Poin kuis harus ditetapkan. + Bagian Kursus + Grup + Siswa + Tambah Penerima Penetapan + Tetapkan Kepada + Tersedia Mulai + Tersedia untuk + Tanggal pembukaan tidak boleh setelah tanggal batas + Tanggal kunci tidak boleh sebelum tanggal batas + Tanggal kunci tidak boleh sebelum tanggal pembukaan + Penerima Penetapan tidak boleh kosong + Hapus + Tambah Batas Waktu + Detail penyerahan lengkap + Dinilai + Membutuhkan Penilaian + Belum Diserahkan + lihat detail penyerahan + Dinilai, %s dari %s + Membutuhkan penilaian, %s dari %s + Tidak diserahkan, %s dari %s + Tampilkan Nilai sebagai… + Persentase + Selesai/Belum Selesai + Poin + Skala IPK + + + @string/percentage + @string/complete_incomplete + @string/points + @string/letter_grade + @string/gpa_scale + @string/not_graded + + + + @string/percentage + @string/complete_incomplete + @string/points + @string/letter_grade + @string/gpa_scale + + + Bukan guru? + Salah satu app lain mungkin lebih cocok. Ketuk untuk membuka App Store. + + Kelas + Komentar + File + File (%d) + Filter Penyerahan + Semua Penyerahan + Diserahkan Terlambat + Belum Diserahkan + Belum Dinilai + Mendapat Skor Kurang Dari … + Mendapat Skor Lebih Dari … + Mendapat Skor Kurang Dari %s + Mendapat Skor Lebih Dari %s + Tambah Komentar + Edit Komentar + Hapus Komentar + Hapus Anotasi + Anda yakin mau menghapus komentar ini? + Lihat deskripsi panjang + Rubrik + Masukkan nilai kustom + Penilaian rubrik disimpan + Kesalahan terjadi saat menyimpan penilaian rubrik. Silakan coba lagi. + %1$s dari %2$s + Sesuaikan Skor + Edit komentar kriteria + Kirim Pesan ke %s + Simpan komentar + Batalkan pengeditan komentar + Lewati + + + Skor khusus %1$s + + + "%1$s, %2$s + + + dari %s poin + dari %s poin + + + Kursus + Opsi Kursus + Edit Kursus Favorit + Formulir Umpan Balik + Lihat Semua + Semua Kursus + Selamat datang! + Tambah beberapa kursus favorit Anda untuk membuat tempat ini serasa rumah sendiri. + Tambah Kursus + Kursus ini tidak dapat ditambahkan ke menu kursus pada saat ini. + Filter Periode Penilaian + Tugas + Bersihkan filter + Batas %1$s + %1$s Diperbarui + + + %s membutuhkan penilaian + %s membutuhkan penilaian + + + Membutuhkan Penilaian + Membutuhkan Penilaian + + di + + %s poin + %s poin + + + %s poin + %s poin + + + %d orang + %d orang + + + %d grup + %d grup + + Ditutup + %s %s + favorit + bukan favorit + Edit nama panggilan + Edit warna kursus + Edit Nama Panggilan Kursus + + Warna Kursus + Ini adalah pengaturan warna pribadi Anda. Hanya Anda yang bisa melihat warna ini untuk kursus. + Warna kursus tidak dapat diatur pada saat ini. + + Merah Muda + Hot Pink + Violet + Ungu + Biru Gelap + Biru + Sian + Aqua Blue + Emerald Green + Hijau + Chartreuse + Kuning + Oranye + Dark Orange + Merah + Jalankan tautan di browser eksternal + File yang Dipilih + Ikon File + Perbesar Halaman + Perkecil Halaman + Tidak Ada Sambungan Internet + Tindakan ini membutuhkan sambungan internet. + + Ketuk satu item pada daftar untuk melihat detail + + Kuis Tugas + Kuis Latihan + Survei Dinilai + Survei + Edit Detail Kuis + Membutuhkan Kode Akses + Kode Akses + Masukkan kode akses atau tidak membutuhkan kode akses. + Kesalahan terjadi saat mencoba menyimpan kuis. Cobalah lagi. + Berhasil memperbarui kuis. + Jenis Kuis + + Kuis Latihan + Kuis Dinilai + Survei Dinilai + Survei Tidak Dinilai + + + @string/practiceQuiz + @string/gradedQuiz + @string/gradedSurvey + @string/ungradedSurvey + + Tidak diambil + + Lengkap + Menunggu Tinjauan + Sedang Berlangsung + Tidak Dimulai + + + Jenis Kuis: + Grup Tugas: + Kocok Jawaban: + Upaya Berganda: + Batas Waktu: + Skor untuk Disimpan: + Upaya: + Lihat Respons: + Tampilkan Jawaban Benar: + Satu Pertanyaan pada Satu Waktu: + Filter IP: + Poin: + Kode Akses: + Penyerahan Anonim: + Kunci Pertanyaan Setelah Menjawab: + + Pratinjau Kuis + Pratinjau Kuis + Detail Kuis + Versi penyerahan + Siswa ini tidak memiliki penyerahan untuk tugas ini. + Grup ini tidak memiliki penyerahan untuk tugas ini. + Jenis penyerahan tidak didukung + Tanpa batas + Kuis Latihan + Kuis Dinilai + Survei Dinilai + Survei Tidak Dinilai + Tanpa Batas Waktu + Segera + Poin tidak ditetapkan + + Dari %1$s hingga %2$s + Setelah %1$s + Hingga %1$s + Masukkan nilai. + + + %d pertanyaan + %d pertanyaan + + + Detail Diskusi + Dipin + Diskusi + Ditutup untuk Komentar + Buka untuk melihat Komentar + Pin + Hapus pin + Opsi lain + Opsi + %s Balasan + %s Belum Dibaca + %s %s %s + Anda yakin mau menghapus balasan ini? + Balasan + Balas + Edit + Balasan hanya tampak oleh mereka yang telah memposting setidaknya satu balasan. + Hapus Diskusi + Hapus Diskusi? + Ini akan menghapus seluruh diskusi dan utas. + + Peringatan Penggunaan Data + Tindakan ini dapat menggunakan data dalam jumlah besar, berpotensi menyebabkan biaya yang tinggi. Anda ingin melanjutkan? + Jangan tampilkan pesan ini lagi + Putar media + Penyerahan ini adalah file media + Coba lagi? + Format media ini tidak didukung + Buka dengan… + + Tugas diserahkan Ketuk untuk melihat + File yang diserahkan: + Tidak ada komentar penyerahan + Unggah Media - Audio + Unggah Media - Video + Upaya %d + Penyerahan Teks + Penyerahan Alat Eksternal + Penyerahan Diskusi + Penyerahan Kuis + File Media + Audio + Video + Penyerahan URL + + File ini tidak dapat ditampilkan. Gunakan tombol di bawah untuk membuka file dengan aplikasi lain di perangkat Anda. + Tugas ini tidak mengizinkan penyerahan. + Tugas ini hanya mengizinkan penyerahan dalam bentuk kertas. + Penyerahan ini adalah URL ke halaman eksternal. Ingatlah bahwa halaman ini mungkin telah berubah sejak penyerahan pertama kali dilakukan. + Buka URL + Kesalahan Anotasi + Anotasi telah dihapus dari sumber lain. + + Pilih kursus + + Kirim pesan individu ke setiap penerima + + Filter Kotak Masuk + Pilih kursus atau grup + + Kembali + + Edit diskusi berhasil. + Pesan diskusi tidak boleh kosong. + Diskusi tidak dapat diedit pada saat ini. + Balasan diskusi berhasil. + Balasan diskusi tidak boleh kosong. + Balasan diskusi tidak dapat dikirim pada saat ini. + Jadilah yang pertama untuk merespons dengan menambahkan balasan. + Tambah penerima lain. Pesan yang dialamatkan hanya untuk diri sendiri tidak dapat dikirim. + Usap kiri atau kanan untuk melihat siswa lain. + Ketuk dan tahan nomor untuk melihat deskripsi. + + Lihat Penyerahan + Penyerahan Nilai + Komentar + Kirim Pesan ke Siswa Yang… + + pada + + Diskusi Baru + Opsi + Berlangganan + Izinkan balasan berutas + Pengguna harus posting sebelum melihat balasan + Izinkan pengguna berkomentar + Judul diskusi harus ditetapkan. + Diskusi berhasil dibuat. + Terjadi kesalahan saat mencoba membuat diskusi. Cobalah lagi. + Edit Diskusi + Diskusi berhasil diperbarui. + Masukkan Teks + Pengumuman berhasil dibuat. + Kesalahan terjadi saat mencoba menyimpan pengumuman ini. Cobalah lagi. + Edit Pengumuman + Pengumuman berhasil diperbarui. + Pengumuman dihapus. + Posting Di + Kesalahan terjadi saat mencoba menghapus pengumuman ini. Cobalah lagi. + Hapus Pengumuman? + Anda yakin mau menghapus pengumuman ini? + Hapus Pengumuman + Kesalahan tak terduga terjadi saat memuat anotasi. + + Akun + Ubah Pengguna + Umum + Kirim Umpan Balik + Kebijakan Privasi + EULA + Ketentuan Penggunaan + Cari di Panduan Canvas + Temukan jawaban untuk pertanyaan umum + Laporkan masalah + Jika aplikasi bermasalah, beri tahu kami + Minta Fitur + Punya ide untuk meningkatkan app? + Bagikan Cinta Anda untuk Aplikasi + Beri tahu kami bagian favorit Anda dari aplikasi + Panduan Canvas + Ide untuk Canvas Teacher [Android] + Informasi berikut akan membantu kami memahami ide Anda lebih baik: + Kirim Email… + Edit Profil + Nama + Bio + Profil telah diperbarui + Tidak dapat memperbarui profil + Ambil foto + Pilih foto dari galeri + File tidak ditemukan. + Versi %s + Versi %s (%d) + Ketuk untuk melihat daftar penyerahan. + Deskripsi Tugas + Deskripsi Kuis + Hapus Tanggal Batas + Ini akan menghapus tanggal batas dan semua penerima penetapan terkait. + Email + layar penuh + Tambah Baru + Kirim Pesan + Kirim Pesan + Kirim pesan ke siswa ini + Gulirkan untuk melihat semua detail + Waktu Batas + Tanggal Mulai Tersedia + Waktu Mulai Tersedia + Tanggal Hingga Tersedia + Waktu Hingga Tersedia + Tambah Deskripsi + Waktu Posting + Semua Orang + Memuat + Sedang menyimpan + Sedang mengirim + Mengunggah + Coba lagi + Ada masalah saat memuat penyerahan ini. + Cari + Filter Orang + Muat Lebih Banyak + Aktivitas terbaru di %1$s pada %2$s. + Tidak dapat memuat detail untuk pengguna ini. + Siswa + Guru + Pengamat + TA + Desainer + Tidak dikenal + Kehadiran tidak dapat dimuat saat ini. + Tandai Sisanya sebagai Hadir + Tandai Semua sebagai Hadir + Kalender + Filter Bagian + Tambah komentar video + Tambah komentar audio + 00:00:00 + %1$d jam, %2$d menit, dan %3$d detik + %1$s dari %2$s + Putar Ulang + Berhenti + Mulai rekaman audio + Stop rekaman audio + Mulai rekaman video + Stop rekaman video + Tutup tampilan rekaman + Hapus rekaman + Putar Ulang Komentar Video + Tambah komentar media + Pemegang Hak Cipta + Akses + Hak Penggunaan + Hapus File + Saya memiliki hak cipta + Saya memiliki izin untuk menggunakan file + File Domain Publik + Pengecualian Penggunaan Wajar + File Creative Commons + Edit File + Hapus Folder + Edit Folder + Anda yakin mau menghapus file ini? + Anda yakin mau menghapus folder ini? Semua konten di folder ini juga akan dihapus. + Folder Baru + Buat Folder + Buat Folder + Buat File + Tampilkan Tombol Buat File dan Buat Folder + Sembunyikan Tombol Buat File dan Buat Folder + Batalkan Terbit + Akses Terbatas + Dibatasi + Tersembunyi, file di dalam akan tersedia dengan tautan. + Hanya tersedia untuk siswa dengan tautan. Tidak tersedia di file siswa. + Jadwalkan ketersediaan siswa + Terjadi kesalahan selama pembuatan folder. + + Atur sebagai Halaman Depan + Dapat Mengedit + Detail Halaman + Halaman berhasil diperbarui. + Halaman berhasil dibuat. + Edit Halaman + Buat Halaman + Judul halaman harus ditetapkan. + Hapus Halaman? + Hapus Halaman + Ini akan menghapus halaman. Tindakan ini tidak bisa diurungkan + Kesalahan terjadi saat mencoba menyimpan halaman ini. Cobalah lagi. + Halaman tidak bisa menjadi halaman depan dan tidak diterbitkan. + + Khusus Guru + Guru dan Siswa + Siapa Saja + Khusus Anggota + Lisensi + Tanggal akhir ketersediaan harus setelah tanggal mulai + Terjadi kesalahan saat menghapus file ini. + Kesalahan terjadi saat menghapus folder ini + Kesalahan terjadi saat memperbarui file ini + Kesalahan terjadi saat memperbarui folder ini + + Tidak ada lagi hal yang harus dilakukan!\n Semoga harimu menyenangkan. + Tidak ada penyerahan untuk dinilai untuk tugas ini. + Kesalahan terjadi saat mencoba melihat item To Do ini. + Pendaftaran desainer tidak dapat melihat ini. + + Bagian + Filter Menurut … + untuk %s + Filter menurut bagian + Filter penyerahan + Penalti terlambat + Filter Nilai + Penilaian termoderasi saat ini tidak didukung dalam mobile SpeedGrader. + + -%s poin + -%s poin + + + -%s poin + -%s poin + + Pengaturan Profil + Mengunduh file… + File berhasil diunduh. + Ketuk untuk melihat + Terjadi kesalahan saat mengunduh file Anda. Silakan coba lagi. + + Posting Ke + Semua Bagian + + Gauge + Alat LTI ini tidak dapat dimuat saat ini. + Mengunduh... + + + Silabus berhasil diperbarui. + Terjadi kesalahan saat mencoba menyimpan silabus. Cobalah lagi. + Edit Silabus + Konten + Detail + Tampilkan rangkuman kursus + Konten silabus + Tanpa nilai + Dibolehkan + Nilai lebih oleh %s + + diff --git a/apps/teacher/src/main/res/values/styles.xml b/apps/teacher/src/main/res/values/styles.xml index 6a5b67577b..dd1aed4c42 100644 --- a/apps/teacher/src/main/res/values/styles.xml +++ b/apps/teacher/src/main/res/values/styles.xml @@ -181,7 +181,7 @@ 7dp true no - @color/backgroundMedium + @color/textDarkest @drawable/ic_canvas_wordmark diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/Randomizer.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/Randomizer.kt index c60281f5c0..32e38aa4cb 100644 --- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/Randomizer.kt +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/Randomizer.kt @@ -122,6 +122,8 @@ object Randomizer { fun randomTextFileContents() = faker.lorem().paragraph(20) + fun randomLargeTextFileContents() = faker.lorem().paragraph(100000) + /** Creates a random page title with a UUID to avoid Canvas URL collisions */ fun randomPageTitle(): String = faker.gameOfThrones().house() + " " + randomUUID() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt index 001c0a9222..9c0b7c01cb 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt @@ -37,6 +37,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.util.HumanReadables import androidx.test.espresso.util.TreeIterables import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult +import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.textfield.TextInputLayout import com.instructure.espresso.ActivityHelper import junit.framework.AssertionFailedError @@ -203,6 +204,30 @@ fun withIndex(matcher: Matcher, index: Int): Matcher { } } +fun withRotation(rotation: Float): Matcher { + return object : TypeSafeMatcher() { + override fun matchesSafely(item: View): Boolean { + return item.rotation == rotation + } + + override fun describeTo(description: Description) { + description.appendText("with rotation: $rotation") + } + } +} + +fun hasCheckedState(checkedState: Int) : Matcher { + return object : TypeSafeMatcher() { + override fun matchesSafely(item: View): Boolean { + return item is MaterialCheckBox && item.checkedState == checkedState + } + + override fun describeTo(description: Description?) { + description?.appendText("has the proper checked state.") + } + } +} + // A matcher for views whose width is less than the specified amount (in dp), // but whose height is at least the specified amount. // This is used to suppress accessibility failures related to overflow menus diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt index 2a8cd358aa..a9a0ed63f7 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt @@ -43,7 +43,16 @@ object CourseListEndpoint : Endpoint( val courses = data.enrollments .values .filter { it.userId == user.id } - .map { data.courses[it.courseId]!! } + .map { + var course = data.courses[it.courseId]!! + if (request.url.queryParameterValues("include[]").contains("tabs")) { + course = course.copy(tabs = data.courseTabs[course.id]) + } + if (request.url.queryParameterValues("include[]").contains("permissions")) { + course.permissions = data.coursePermissions[course.id] + } + course + } .filter { when (enrollmentState) { "active" -> it.isCurrentEnrolment() @@ -112,7 +121,14 @@ object CourseEndpoint : Endpoint( response = { GET { - val course = data.courses[pathVars.courseId]!! + val courseId = pathVars.courseId + var course = data.courses[courseId]!! + if (request.url.queryParameterValues("include[]").contains("tabs")) { + course = course.copy(tabs = data.courseTabs[courseId]) + } + if (request.url.queryParameterValues("include[]").contains("permissions")) { + course.permissions = data.coursePermissions[courseId] + } val userId = request.user!!.id if (data.enrollments.values.any { it.courseId == course.id && it.userId == userId }) { request.successResponse(course) diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt index 92a6c1f62d..0951e8236d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt @@ -20,6 +20,7 @@ import android.graphics.Color import android.view.View import android.widget.TextView import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewAssertion @@ -28,6 +29,7 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.viewpager.widget.ViewPager import com.google.android.material.bottomnavigation.BottomNavigationView import junit.framework.AssertionFailedError +import org.hamcrest.Matcher import org.hamcrest.Matchers import org.junit.Assert.assertEquals @@ -93,4 +95,28 @@ class DoesNotExistAssertion(private val timeoutInSeconds: Long, private val poll throw AssertionError("View still exists after $timeoutInSeconds seconds.") } -} \ No newline at end of file +} + +class ConstraintLayoutItemCountAssertionWithMatcher(private val matcher: Matcher, private val expectedCount: Int) : ViewAssertion { + override fun check(view: View, noViewFoundException: NoMatchingViewException?) { + noViewFoundException?.let { throw it } + if (view !is ConstraintLayout) { + throw ClassCastException("View of type ${view.javaClass.simpleName} must be a ConstraintLayout") + } + val count = (0 until view.childCount) + .map { view.getChildAt(it) }.count { matcher.matches(it) } + ViewMatchers.assertThat(count, Matchers.`is`(expectedCount)) + } +} + +class ConstraintLayoutItemCountAssertion(private val expectedCount: Int) : ViewAssertion { + override fun check(view: View, noViewFoundException: NoMatchingViewException?) { + noViewFoundException?.let { throw it } + if (view !is ConstraintLayout) { + throw ClassCastException("View of type ${view.javaClass.simpleName} must be a ConstraintLayout") + } + val count = view.childCount + ViewMatchers.assertThat(count, Matchers.`is`(expectedCount)) + } +} + diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/actions/ForceClick.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/actions/ForceClick.kt new file mode 100644 index 0000000000..a69bc625b9 --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/actions/ForceClick.kt @@ -0,0 +1,50 @@ +// +// Copyright (C) 2023-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.espresso.actions + + +import android.view.View +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.Matcher + + +/** + * During Espresso click the coordinates calculation may have been broken by setRotation call on the view. + * This forceClick will perform click without checking the coordinates. + * + */ +class ForceClick : ViewAction { + + override fun getConstraints(): Matcher? { + return allOf(isClickable(), isEnabled(), isDisplayed()) + } + + override fun getDescription(): String? { + return "force click" + } + + override fun perform(uiController: UiController, view: View) { + view.performClick() // perform click without checking view coordinates. + uiController.loopMainThreadUntilIdle() + } + +} diff --git a/libs/annotations/src/main/res/values-id/strings.xml b/libs/annotations/src/main/res/values-id/strings.xml new file mode 100644 index 0000000000..24883e9e30 --- /dev/null +++ b/libs/annotations/src/main/res/values-id/strings.xml @@ -0,0 +1,42 @@ + + + + + Masukkan Teks + Batal + Komentar + Lewati + Komentar + Ada masalah saat memuat penyerahan ini. + Coba lagi + Kesalahan tak terduga terjadi saat memuat anotasi. + Kesalahan Anotasi + Anotasi telah dihapus dari sumber lain. + Pesan ini tidak dapat dikirim. Ketuk untuk mencoba lagi. + Tambah Komentar + Edit Komentar + Hapus Komentar + Hapus Anotasi + Anda yakin mau menghapus komentar ini? + Menghapus komentar ini akan menghapus semua komentar yang berkaitan dengan anotasi ini. Anda yakin mau melakukan ini? + Sedang mengirim + Catat Warna + Menghapus %1$s oleh %2$s + + + diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt index 511e8c4759..0f8beec278 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt @@ -89,6 +89,9 @@ object FileFolderAPI { @GET("{canvasContext}/files") fun searchFiles(@Path(value = "canvasContext", encoded = true) contextPath: String, @Query("search_term") query: String): Call> + @GET("{canvasContext}/files") + suspend fun searchFiles(@Path(value = "canvasContext", encoded = true) contextPath: String, @Query("search_term") query: String, @Tag params: RestParams): DataResult> + @DELETE("files/{fileId}") fun deleteFile(@Path("fileId") fileId: Long): Call diff --git a/libs/canvas-api-2/src/main/res/values-id/strings.xml b/libs/canvas-api-2/src/main/res/values-id/strings.xml new file mode 100644 index 0000000000..c4c6d36293 --- /dev/null +++ b/libs/canvas-api-2/src/main/res/values-id/strings.xml @@ -0,0 +1,80 @@ + + + + + + + Anda pertama-tama harus menyelesaikan: + Ini akan terbuka pada: + Pergi Ke Modul + + + Memulai + Mengakhiri + Acara Satu Hari Penuh + di + kepada + + + Pesan + Memuat… + Dihapus + + + Online + Di Atas Kertas + Diskusi + Kuis + Alat Eksternal + Tidak Ada Penyerahan + + Lulus Gagal + Persen + Nilai Dalam Huruf + Poin + Skala IPK + Tidak Dinilai + + Kuis + Topik Diskusi + Unggahan File + Entri Teks + URL Situs Web + Rekaman Media + Kehadiran + Anotasi Siswa + Tertinggi + Rata-Rata + Terbaru + Tidak + Selalu + Kuis Dinilai + Kuis Latihan + Survei Dinilai + Survei Tidak Dinilai + \u0020dan %d lainnya + + + Siswa + Guru + Pengamat + TA + Desainer + Tidak dikenal + + diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_id.arb b/libs/flutter_student_embed/lib/l10n/res/intl_id.arb new file mode 100644 index 0000000000..d8ec08b159 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_id.arb @@ -0,0 +1,447 @@ +{ + "@@last_modified": "2022-10-28T11:03:17.232435", + "coursesLabel": "Kursus", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalender", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Bulan selanjutnya: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Bulan sebelumnya: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Minggu selanjutnya mulai {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Minggu sebelumnya mulai {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Bulan dari {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "perbesar", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "perkecil", + "@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} poin memungkinkan", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "No Events Today!": "Tidak Ada Acara Hari Ini!", + "@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.": "Sepertinya hari ini sangat cocok untuk beristirahat, santai, dan menyegarkan diri.", + "@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 calendar": "Terjadi kesalahan saat memuat kalender Anda", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Pilih elemen untuk ditampilkan di kalender.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Pergi ke hari ini", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Harus Dilakukan", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Belum ada deskripsi", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Tanggal", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "To Do Baru", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit To Do", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Judul", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kursus (opsional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Tidak ada", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Deskripsi", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Simpan", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Anda Yakin?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Anda mau menghapus Item To-do ini?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Perubahan belum disimpan", + "@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 unsaved changes will be lost.": "Anda yakin mau menutup halaman ini? Perubahan yang belum disimpan akan hilang.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Judul tidak boleh kosong", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Terjadi kesalahan saat menyimpan To Do ini. Silakan periksa sambungan internet Anda dan coba lagi.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Terjadi kesalahan saat menghapus To Do ini. Silakan periksa sambungan internet Anda dan coba lagi.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "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.": "Kami tidak yakin apa yang terjadi, tetapi hal itu tidak baik. Hubungi kami jika ini terus terjadi.", + "@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": {} + }, + "View error details": "Lihat detail kesalahan", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versi aplikasi", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model perangkat", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versi 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": "Pesan kesalahan lengkap", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Tidak Ada Kursus", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Kursus siswa Anda mungkin belum diterbitkan.", + "@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 your student’s courses.": "Terjadi kesalahan ketika memuat kursus siswa Anda.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "To Do {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Dinilai", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Diserahkan", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} poin", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Dibolehkan", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Tidak Ada", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Batal", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ya", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Tidak", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Coba lagi", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Hapus", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Selesai", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} di {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Batas waktu {date} pada {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt index f8ffb23c92..c669b02bcd 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt @@ -225,7 +225,6 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi ColorUtils.colorIt(color, canvasLogo) // App Name/Type. Will not be present in all layout versions - canvasWordmark.imageTintList = ColorStateList.valueOf(color) appDescriptionType.setText(appTypeName()) ViewStyler.themeStatusBar(this@BaseLoginLandingPageActivity) diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt index b59928dcd8..b3111f8ece 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt @@ -54,6 +54,8 @@ abstract class LogoutTask( protected abstract fun getFcmToken(listener: (registrationId: String?) -> Unit) protected abstract fun removeOfflineData(userId: Long?) + protected open fun stopOfflineSync() = Unit + @Suppress("EXPERIMENTAL_FEATURE_WARNING") fun execute() { try { @@ -78,6 +80,8 @@ abstract class LogoutTask( } PushNotification.clearPushHistory() + stopOfflineSync() + when (type) { Type.LOGOUT, Type.LOGOUT_NO_LOGIN_FLOW -> { removeOfflineData(ApiPrefs.user?.id) diff --git a/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml b/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml index a9de871d69..42ad6f9a69 100644 --- a/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml +++ b/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml @@ -55,7 +55,7 @@ android:layout_marginBottom="2dp" android:adjustViewBounds="true" android:importantForAccessibility="no" - android:tint="@color/login_teacherAppTheme" + android:tint="@color/textDarkest" app:srcCompat="@drawable/ic_canvas_wordmark" /> school.instructure.com حذف المستخدم السابق ألا تستطيع العثور على مدرستك؟ حاول كتابة رابط URL الكامل للمدرسة. - اضغط هنا للحصول على المساعدة. - + اضغط هنا للاطلاع على تعليمات تسجيل الدخول. لا يوجد اتصال بالإنترنت يتطلب هذا الإجراء اتصالاً بالإنترنت. يلزم وجود موضوع ووصف لإرسال الملاحظات والآراء. diff --git a/libs/login-api-2/src/main/res/values-b+da+DK+instk12/strings.xml b/libs/login-api-2/src/main/res/values-b+da+DK+instk12/strings.xml index c14f873ae4..614181871c 100644 --- a/libs/login-api-2/src/main/res/values-b+da+DK+instk12/strings.xml +++ b/libs/login-api-2/src/main/res/values-b+da+DK+instk12/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Fjern forrige bruger Kan du ikke finde din skole? Prøv at indtaste skolens fulde URL. - Tryk her for at få hjælp. - + Tryk her for at få hjælp til login. Ingen internetforbindelse Denne handling kræver en internetforbindelse. Der kræves et emne og en beskrivelse for at indsende feedback. diff --git a/libs/login-api-2/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/login-api-2/src/main/res/values-b+en+AU+unimelb/strings.xml index 562a29b642..ab8456824c 100644 --- a/libs/login-api-2/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/login-api-2/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Remove Previous User Can\'t find your school? Try typing the full school URL. - Tap here for help. - + Tap here for login help. No Internet Connection This action requires an internet connection. A subject and description are required to submit feedback. diff --git a/libs/login-api-2/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/login-api-2/src/main/res/values-b+en+GB+instukhe/strings.xml index 2008cc1293..d63ec11a9d 100644 --- a/libs/login-api-2/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/login-api-2/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Remove Previous User Can\'t find your school? Try typing the full school URL. - Tap here for help. - + Tap here for login help. No Internet Connection This action requires an internet connection. A subject and description are required to submit feedback. diff --git a/libs/login-api-2/src/main/res/values-b+nb+NO+instk12/strings.xml b/libs/login-api-2/src/main/res/values-b+nb+NO+instk12/strings.xml index ec5a6c20af..ae1b8fc450 100644 --- a/libs/login-api-2/src/main/res/values-b+nb+NO+instk12/strings.xml +++ b/libs/login-api-2/src/main/res/values-b+nb+NO+instk12/strings.xml @@ -110,8 +110,7 @@ school.instructure.com Fjerne forrige bruker Finner du ikke skolen din? Prøv å skrive hele skolens URL. - Trykk her for hjelp. - + Trykk her for innloggingshjelp. Ingen Internett-tilkobling Denne handlingen krever Internett-tilkobling. Et tittel og en beskrivelse må sende inn tilbakemelding. diff --git a/libs/login-api-2/src/main/res/values-b+sv+SE+instk12/strings.xml b/libs/login-api-2/src/main/res/values-b+sv+SE+instk12/strings.xml index 6545349e99..e789de3137 100644 --- a/libs/login-api-2/src/main/res/values-b+sv+SE+instk12/strings.xml +++ b/libs/login-api-2/src/main/res/values-b+sv+SE+instk12/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Ta bort tidigare användare Kan du inte hitta din skola? Försök att skriva in skolans fullständiga URL. - Tryck här för hjälp. - + Tryck här för inloggningshjälp. Ingen internetanslutning Den här åtgärden kräver internetanslutning. Ett ämne och en beskrivning krävs för att skicka feedback. diff --git a/libs/login-api-2/src/main/res/values-b+zh+HK/strings.xml b/libs/login-api-2/src/main/res/values-b+zh+HK/strings.xml index 7617208253..37d1ee5029 100644 --- a/libs/login-api-2/src/main/res/values-b+zh+HK/strings.xml +++ b/libs/login-api-2/src/main/res/values-b+zh+HK/strings.xml @@ -109,8 +109,7 @@ school.instructure.com 移除先前使用者 找不到您的學校?請嘗試輸入學校完整URL。 - 點擊此處獲取支援。 - + 點選此處獲得登入協助。 沒有網絡連線 此動作需要網絡連線。 需有主題和描述方可提交反饋。 diff --git a/libs/login-api-2/src/main/res/values-b+zh+Hans/strings.xml b/libs/login-api-2/src/main/res/values-b+zh+Hans/strings.xml index 871b422f6d..6c791c527b 100644 --- a/libs/login-api-2/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/login-api-2/src/main/res/values-b+zh+Hans/strings.xml @@ -109,8 +109,7 @@ school.instructure.com 删除上一个用户 找不到您的学校?尝试键入完整的学校 URL。 - 轻击此处获取帮助。 - + 点击此处获取登录帮助。 无互联网连接 此操作需要互联网连接。 须填写主题和描述后才能提交反馈。 diff --git a/libs/login-api-2/src/main/res/values-b+zh+Hant/strings.xml b/libs/login-api-2/src/main/res/values-b+zh+Hant/strings.xml index 7617208253..37d1ee5029 100644 --- a/libs/login-api-2/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/login-api-2/src/main/res/values-b+zh+Hant/strings.xml @@ -109,8 +109,7 @@ school.instructure.com 移除先前使用者 找不到您的學校?請嘗試輸入學校完整URL。 - 點擊此處獲取支援。 - + 點選此處獲得登入協助。 沒有網絡連線 此動作需要網絡連線。 需有主題和描述方可提交反饋。 diff --git a/libs/login-api-2/src/main/res/values-ca/strings.xml b/libs/login-api-2/src/main/res/values-ca/strings.xml index 36169a8672..0dd8cc4771 100644 --- a/libs/login-api-2/src/main/res/values-ca/strings.xml +++ b/libs/login-api-2/src/main/res/values-ca/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Eliminar l\'usuari anterior No trobeu la vostra escola? Proveu d\'escriure l\'URL de l\'escola complet. - Toqueu aquí per obtenir ajuda. - + Toqueu aquí per obtenir ajuda per iniciar la sessió. No hi ha cap connexió a Internet Per dur a terme aquesta acció cal tenir una connexió a Internet. Per enviar comentaris cal indicar un assumpte i una descripció. diff --git a/libs/login-api-2/src/main/res/values-cy/strings.xml b/libs/login-api-2/src/main/res/values-cy/strings.xml index 0e1d15dd39..a00f202419 100644 --- a/libs/login-api-2/src/main/res/values-cy/strings.xml +++ b/libs/login-api-2/src/main/res/values-cy/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Tynnu defnyddiwr blaenorol? Methu dod o hyd i’ch ysgol? Ceisiwch deipio URL llawn yr ysgol. - Tapiwch yma i gael help. - + Tapiwch yma i gael help gyda mewngofnodi. Dim cysylltiad â\'r rhyngrwyd Mae angen cysylltiad â\'r rhyngrwyd i wneud hyn. Mae’n rhaid rhoi pwnc a disgrifiad er mwyn cyflwyno adborth. diff --git a/libs/login-api-2/src/main/res/values-da/strings.xml b/libs/login-api-2/src/main/res/values-da/strings.xml index 1faf265adb..159790e77e 100644 --- a/libs/login-api-2/src/main/res/values-da/strings.xml +++ b/libs/login-api-2/src/main/res/values-da/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Fjern forrige bruger Kan du ikke finde din skole? Prøv at indtaste skolens fulde URL. - Tryk her for at få hjælp. - + Tryk her for at få hjælp til login. Ingen internetforbindelse Denne handling kræver en internetforbindelse. Der kræves et emne og en beskrivelse for at indsende feedback. diff --git a/libs/login-api-2/src/main/res/values-de/strings.xml b/libs/login-api-2/src/main/res/values-de/strings.xml index 8389e442df..d950d85190 100644 --- a/libs/login-api-2/src/main/res/values-de/strings.xml +++ b/libs/login-api-2/src/main/res/values-de/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Vorherige/n Benutzer*in entfernen Finden Sie Ihre Schule nicht? Geben Sie den vollständigen URL der Schule ein. - Tippen Sie hier, um Hilfe zu erhalten. - + Für Hilfe beim Login hier tippen. Keine Internetverbindung Diese Aktion erfordert eine Internetverbindung. Ein Betreff und eine Beschreibung sind erforderlich, um ein Feedback zu geben. diff --git a/libs/login-api-2/src/main/res/values-en-rAU/strings.xml b/libs/login-api-2/src/main/res/values-en-rAU/strings.xml index 562a29b642..ab8456824c 100644 --- a/libs/login-api-2/src/main/res/values-en-rAU/strings.xml +++ b/libs/login-api-2/src/main/res/values-en-rAU/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Remove Previous User Can\'t find your school? Try typing the full school URL. - Tap here for help. - + Tap here for login help. No Internet Connection This action requires an internet connection. A subject and description are required to submit feedback. diff --git a/libs/login-api-2/src/main/res/values-en-rCY/strings.xml b/libs/login-api-2/src/main/res/values-en-rCY/strings.xml index 2008cc1293..d63ec11a9d 100644 --- a/libs/login-api-2/src/main/res/values-en-rCY/strings.xml +++ b/libs/login-api-2/src/main/res/values-en-rCY/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Remove Previous User Can\'t find your school? Try typing the full school URL. - Tap here for help. - + Tap here for login help. No Internet Connection This action requires an internet connection. A subject and description are required to submit feedback. diff --git a/libs/login-api-2/src/main/res/values-en-rGB/strings.xml b/libs/login-api-2/src/main/res/values-en-rGB/strings.xml index 4efa16bd21..77b12d10a1 100644 --- a/libs/login-api-2/src/main/res/values-en-rGB/strings.xml +++ b/libs/login-api-2/src/main/res/values-en-rGB/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Remove Previous User Can\'t find your school? Try typing the full school URL. - Tap here for help. - + Tap here for login help. No Internet Connection This action requires an internet connection. A subject and description are required to submit feedback. diff --git a/libs/login-api-2/src/main/res/values-es-rES/strings.xml b/libs/login-api-2/src/main/res/values-es-rES/strings.xml index 34f0ec1e70..1d68333789 100644 --- a/libs/login-api-2/src/main/res/values-es-rES/strings.xml +++ b/libs/login-api-2/src/main/res/values-es-rES/strings.xml @@ -110,8 +110,7 @@ school.instructure.com Eliminar usuario anterior ¿No puedes encontrar tu escuela? Intenta escribir la URL completa de la escuela. - Toca aquí para obtener ayuda. - + Toca aquí para obtener ayuda para iniciar sesión. No hay conexión a Internet Esta acción requiere conexión a Internet. Es necesario un tema y una descripción para enviar comentarios. diff --git a/libs/login-api-2/src/main/res/values-es/strings.xml b/libs/login-api-2/src/main/res/values-es/strings.xml index f87d909f6e..87ae67529b 100644 --- a/libs/login-api-2/src/main/res/values-es/strings.xml +++ b/libs/login-api-2/src/main/res/values-es/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Eliminar usuario anterior ¿No puede encontrar su escuela? Intente escribir la URL completa de la escuela. - Presione aquí para obtener ayuda. - + Presione aquí para obtener ayuda con el inicio de sesión. Sin conexión a Internet Esta acción requiere conexión a Internet. Es necesario un tema y una descripción para enviar comentarios. diff --git a/libs/login-api-2/src/main/res/values-fi/strings.xml b/libs/login-api-2/src/main/res/values-fi/strings.xml index 4157a60613..be919d01e5 100644 --- a/libs/login-api-2/src/main/res/values-fi/strings.xml +++ b/libs/login-api-2/src/main/res/values-fi/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Poista edellinen käyttäjä Etkö löydä kouluasi? Yritä kirjoittaa koko koulun URL - Avaa ohje napauttamalla tässä. - + Avaa sisäänkirjautumisen ohje napauttamalla tässä. Ei Internet-yhteyttä Tähän toimintoon vaaditaan Internet-yhteys. Aihe ja kuvaus vaaditaan palautteen lähettämiseen. diff --git a/libs/login-api-2/src/main/res/values-fr-rCA/strings.xml b/libs/login-api-2/src/main/res/values-fr-rCA/strings.xml index 060153b513..e1442ab6da 100644 --- a/libs/login-api-2/src/main/res/values-fr-rCA/strings.xml +++ b/libs/login-api-2/src/main/res/values-fr-rCA/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Retirer l\'utilisateur précédent Vous ne trouvez pas votre école? Essayez de saisir l\'URL complète de l’école. - Appuyer ici pour obtenir de l\'aide. - + Cliquez ici pour obtenir de l’aide à la connexion. Aucune connexion Internet Cette action nécessite une connexion Internet. Un sujet et une description sont requis afin de soumettre des commentaires. diff --git a/libs/login-api-2/src/main/res/values-fr/strings.xml b/libs/login-api-2/src/main/res/values-fr/strings.xml index d2a7dedb52..3f7ae22397 100644 --- a/libs/login-api-2/src/main/res/values-fr/strings.xml +++ b/libs/login-api-2/src/main/res/values-fr/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Supprimer utilisateur précédent Vous ne trouvez pas votre école ? Essayez d’entrer l’URL complète de l’école - Appuyez ici pour obtenir de l’aide. - + Appuyer ici pour afficher l’aide à la connexion. Aucune connexion internet Cette action nécessite une connexion Internet. Vous devez entrer un sujet et une description pour envoyer un retour. diff --git a/libs/login-api-2/src/main/res/values-ht/strings.xml b/libs/login-api-2/src/main/res/values-ht/strings.xml index 2eea2ac773..cb5c2b0e1a 100644 --- a/libs/login-api-2/src/main/res/values-ht/strings.xml +++ b/libs/login-api-2/src/main/res/values-ht/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Elimine Ansyen Itilizatè Ou paka jwenn lekòl ou a? Eseye tape tout URL lekòl la - Tape la pou èd. - + Tape la pou w jwenn èd pou w ka konekte. Pa gen Koneksyon Internet Pou aksyon sa a ou bezwen konekte sou entènèt. Ou dwe gen yon sijè ak yon deskripsyon pou soumèt kòmantè sa a. diff --git a/libs/login-api-2/src/main/res/values-id/strings.xml b/libs/login-api-2/src/main/res/values-id/strings.xml new file mode 100644 index 0000000000..7433318e75 --- /dev/null +++ b/libs/login-api-2/src/main/res/values-id/strings.xml @@ -0,0 +1,131 @@ + + + + + + Halo dunia! + Pengaturan + Aplikasi ini tidak diizinkan untuk digunakan + Server yang Anda masukkan tidak diizinkan untuk aplikasi ini. + Agen pengguna untuk aplikasi ini tidak diizinkan. + Kami tidak dapat memverifikasi server untuk digunakan bersama aplikasi ini. + UnknownDevice + Terjadi kesalahan yang tidak terduga. + Halaman tidak ditemukan + Konfirmasi + Batal + Bantu + Masukkan URL Canvas Anda: + Coba masuk ke Canvas Network? + Hapus + myschool.instructure.com + ID Pengguna + Domain + Bertindak sebagai Pengguna + Stop Bertindak sebagai Pengguna + + + Hapus pengguna ini? + Anda harus masuk kembali ke pengguna ini lagi untuk mengakses kontennya. + + + Logo Canvas + + Canvas_Login: HIDUP + Canvas_Login: MATI + Login Admin Situs: HIDUP + + Nama pengguna + Kata sandi + Autentikasi Dibutuhkan + Email atau kata sandi tidak valid + Login + Logout + Mencocokkan Sekolah + + + Anda yakin mau logout? + Ya + Tidak + + + Di Dekat Anda + Jaringan Canvas + Tepat di belakang Anda + Tidak melihat sekolah Anda? Masukkan URL sekolah atau ketuk di sini untuk mendapat bantuan. + Masukkan URL sekolah atau ketuk di sini untuk mendapat bantuan. + Temukan sekolah atau distrik Anda + + + Laporkan Masalah + Subjek + Deskripsi + Kirim + Terima kasih Anda sudah melaporkan masalah Anda kepada Tim Dukungan. Anda akan menerima verifikasi dan info terbaru melalui email. + Bagaimana ini berpengaruh pada Anda? + Hanya pertanyaan biasa, komentar, ide, saran... + Saya butuh bantuan tetapi tidak mendesak. + Ada yang terputus tetapi bisa saya cari cara untuk mendapat apa yang saya butuhkan. + Saya tidak bisa melakukan apa pun sampai saya mendapat info dari Anda. + DARURAT KRITIKAL EKSTREM!! + + Alamat Email + Tidak dikenal + + + < 1 mil (1,6 km) + 1 mil (1,6 km) + mil + km + + Perangkat + Versi OS + + Login Sebelumnya + Temukan sekolah saya + Kode QR + Temukan Kode QR + Anda akan menemukan kode QR di web di profil akun Anda. Klik \'QR untuk Login Seluler\' di dalam daftar. + Pindai kode QR dari Canvas untuk login + Terjadi kesalahan saat login. Silakan buat Kode QR lain dan coba lagi. + Silakan pindai kode QR yang dibuat oleh Canvas + Tangkapan layar yang menampilkan lokasi pembuatan kode QR di browser + Apa nama sekolah Anda? + Berikutnya + school.instructure.com + Hapus Pengguna Sebelumnya + Tidak dapat menemukan sekolah Anda? Coba ketikkan URL sekolah lengkap. + Ketuk di sini untuk bantuan. + + Tidak Ada Sambungan Internet + Tindakan ini membutuhkan sambungan internet. + Subjek dan deskripsi harus ada untuk menyerahkan umpan balik. + Nomor Versi + Gulirkan untuk melihat semua detail + + Anda harus memasukkan id pengguna. + Anda harus memasukkan domain yang valid + Kesalahan saat mencoba bertindak sebagai pengguna. + \"Bertindak sebagai\" pada dasarnya adalah melakukan login sebagai pengguna ini tanpa kata sandi. Anda akan dapat melakukan tindakan apa pun jika Anda adalah pengguna ini, dan dari sudut pandang pengguna lain, ini seakan-akan pengguna ini melakukannya. Namun, log audit mencatat bahwa Anda lah orang yang melakukan tindakan atas nama pengguna ini. + Anda bertindak sebagai %s + Stop bertindak sebagai... + Anda akan berhenti bertindak sebagai %s dan kembali ke akun Anda. + Kami telah membuat beberapa perubahan. + Lihat apa yang baru + Temukan sekolah lain + diff --git a/libs/login-api-2/src/main/res/values-is/strings.xml b/libs/login-api-2/src/main/res/values-is/strings.xml index ab6760cb70..4b1355a3e1 100644 --- a/libs/login-api-2/src/main/res/values-is/strings.xml +++ b/libs/login-api-2/src/main/res/values-is/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Fjarlægja fyrri notanda Finnurðu ekki skólann þinn? Prufaðu að slá inn alla vefslóð skólans. - Smelltu hér til að fá hjálp. - + Smelltu hér fyrir hjálp við innskráningu. Engin nettenging Þessi aðgerð krefst nettengingar. Efni og lýsingar er krafist til að senda inn endurgjöf. diff --git a/libs/login-api-2/src/main/res/values-it/strings.xml b/libs/login-api-2/src/main/res/values-it/strings.xml index 541ff2dc4e..6b1278a2d7 100644 --- a/libs/login-api-2/src/main/res/values-it/strings.xml +++ b/libs/login-api-2/src/main/res/values-it/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Rimuovi utente precedente Non riesci a trova la tua scuola? Prova a digitare l’URL della scuola per intero. - Tocca qui per assistenza. - + Tocca qui per la guida all’accesso. Nessuna connessione a Internet Questa azione richiede una connessione a Internet. Per inviare un feedback sono necessari argomento e descrizione. diff --git a/libs/login-api-2/src/main/res/values-ja/strings.xml b/libs/login-api-2/src/main/res/values-ja/strings.xml index 01da4c3a5d..7afda5eb40 100644 --- a/libs/login-api-2/src/main/res/values-ja/strings.xml +++ b/libs/login-api-2/src/main/res/values-ja/strings.xml @@ -109,8 +109,7 @@ school.instructure.com 以前のユーザーを削除 学校が見つかりませんか?学校の完全なURLを入力してみてください。 - ここをタップしてヘルプを表示します。 - + ここをタップするとログインヘルプが表示されます。 インターネット接続なし この操作にはインターネット接続が必要です。 フィードバックを提出するには、件名と説明は必須です。 diff --git a/libs/login-api-2/src/main/res/values-mi/strings.xml b/libs/login-api-2/src/main/res/values-mi/strings.xml index 772695e3af..2c46d3a8b7 100644 --- a/libs/login-api-2/src/main/res/values-mi/strings.xml +++ b/libs/login-api-2/src/main/res/values-mi/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Tango Kaiwhakmahi o mua Kaore e kitea tō kura? Ngana ki te pātō te nuinga o te kura url - Pātō ki kōnei mo te awhina - + Pato ki konei mo te awhina takiuru. Kaore he hononga ipurangi Tēnei mahi ka mahi he hononga ipurangi E hiahiatia ana te kaupapa me te whakamārama ki te tuku urupare. diff --git a/libs/login-api-2/src/main/res/values-ms/strings.xml b/libs/login-api-2/src/main/res/values-ms/strings.xml index b8be733a44..1f9b5e2efd 100644 --- a/libs/login-api-2/src/main/res/values-ms/strings.xml +++ b/libs/login-api-2/src/main/res/values-ms/strings.xml @@ -110,8 +110,7 @@ school.instructure.com Alih Keluar Pengguna Sebelumnya Tidak menemui sekolah anda? Cuba taip URL sekolah yang penuh. - Ketik di sini untuk mendapatkan bantuan. - + Ketik di sini untuk bantuan log masuk. Tiada Sambungan Internet Tindakan ini memerlukan sambungan Internet Subjek dan penerangan diperlukan untuk menyerahkan maklum balas. diff --git a/libs/login-api-2/src/main/res/values-nb/strings.xml b/libs/login-api-2/src/main/res/values-nb/strings.xml index 1d7d7e6851..577c308b47 100644 --- a/libs/login-api-2/src/main/res/values-nb/strings.xml +++ b/libs/login-api-2/src/main/res/values-nb/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Fjerne forrige bruker Finner du ikke skolen din? Prøv å skrive hele skolens URL. - Trykk her for hjelp. - + Trykk her for innloggingshjelp. Ingen Internett-tilkobling Denne handlingen krever Internett-tilkobling. Et tittel og en beskrivelse må sende inn tilbakemelding. diff --git a/libs/login-api-2/src/main/res/values-nl/strings.xml b/libs/login-api-2/src/main/res/values-nl/strings.xml index 0e4ae4a8f3..adf79b35ac 100644 --- a/libs/login-api-2/src/main/res/values-nl/strings.xml +++ b/libs/login-api-2/src/main/res/values-nl/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Vorige gebruiker verwijderen Kun je je school niet vinden? Typ de volledige URL van de school. - Tik hier voor hulp. - + Tik hier voor hulp bij het aanmelden. Geen internetverbinding Voor deze actie is een internetverbinding vereist. Bij het indienen van de feedback zijn een onderwerp en beschrijving verplicht. diff --git a/libs/login-api-2/src/main/res/values-pl/strings.xml b/libs/login-api-2/src/main/res/values-pl/strings.xml index 552807641f..7fedb3cc5a 100644 --- a/libs/login-api-2/src/main/res/values-pl/strings.xml +++ b/libs/login-api-2/src/main/res/values-pl/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Usuń poprzedniego użytkownika Nie możesz znaleźć swojej szkoły? Spróbuj wprowadzić pełen adres URL szkoły. - Stuknij tutaj, aby uzyskać pomoc. - + Stuknij tutaj, aby uzyskać pomoc przy logowaniu. Brak połączenia internetowego To działanie wymaga połączenia internetowego. W celu przesłania informacji zwrotnych należy wprowadzić temat i opis. diff --git a/libs/login-api-2/src/main/res/values-pt-rBR/strings.xml b/libs/login-api-2/src/main/res/values-pt-rBR/strings.xml index 24542db009..9bff03a13c 100644 --- a/libs/login-api-2/src/main/res/values-pt-rBR/strings.xml +++ b/libs/login-api-2/src/main/res/values-pt-rBR/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Remover usuário anterior Não consegue localizar a sua escola? Tente digitar a URL completa da escola. - Toque aqui para ajuda. - + Toque aqui para obter ajuda de login. Sem conexão à internet Esta ação exige conexão à internet. Um assunto e descrição são necessários para enviar feedback. diff --git a/libs/login-api-2/src/main/res/values-pt-rPT/strings.xml b/libs/login-api-2/src/main/res/values-pt-rPT/strings.xml index e6147e54df..d2c06582f0 100644 --- a/libs/login-api-2/src/main/res/values-pt-rPT/strings.xml +++ b/libs/login-api-2/src/main/res/values-pt-rPT/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Remover utilizador anterior? Não vê sua escola? Tente digitar o URL completo da escola. - Toque aqui para ajuda. - + Toque aqui para obter ajuda do início de sessão. Sem ligação à Internet Esta ação requer uma conexão com a internet. Um assunto e descrição são exigidos para submeter feedback. diff --git a/libs/login-api-2/src/main/res/values-ru/strings.xml b/libs/login-api-2/src/main/res/values-ru/strings.xml index b2730ba446..e1e8f24dc8 100644 --- a/libs/login-api-2/src/main/res/values-ru/strings.xml +++ b/libs/login-api-2/src/main/res/values-ru/strings.xml @@ -109,8 +109,7 @@ school.instructure.com Удалить предыдущего пользователя Не удается найти свое учебное заведение? Попробуйте набрать полный адрес URL учебного заведения. - Прикоснитесь здесь для получения помощи. - + Нажмите здесь для получения справки по входу в систему. Нет интернет-соединения Для выполнения этого действия необходимо интернет-соединение. Для отправки отзыва необходимо указать тему и дать описание. diff --git a/libs/login-api-2/src/main/res/values-sl/strings.xml b/libs/login-api-2/src/main/res/values-sl/strings.xml index 22206a77c5..46b4ee028b 100644 --- a/libs/login-api-2/src/main/res/values-sl/strings.xml +++ b/libs/login-api-2/src/main/res/values-sl/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Odstrani predhodnega uporabnika Ali svoje šole ne najdete? Poskusite vnesti poln naslov URL šole. - Za pomoč tapnite tukaj. - + Za pomoč pri prijavi tapnite tukaj. Brez internetne povezave. Pri tem dejanju je potrebna internetna povezava. Za pošiljanje povratnih informacij sta potrebna predmet in opis. diff --git a/libs/login-api-2/src/main/res/values-sv/strings.xml b/libs/login-api-2/src/main/res/values-sv/strings.xml index 6545349e99..e789de3137 100644 --- a/libs/login-api-2/src/main/res/values-sv/strings.xml +++ b/libs/login-api-2/src/main/res/values-sv/strings.xml @@ -111,8 +111,7 @@ school.instructure.com Ta bort tidigare användare Kan du inte hitta din skola? Försök att skriva in skolans fullständiga URL. - Tryck här för hjälp. - + Tryck här för inloggningshjälp. Ingen internetanslutning Den här åtgärden kräver internetanslutning. Ett ämne och en beskrivning krävs för att skicka feedback. diff --git a/libs/login-api-2/src/main/res/values-th/strings.xml b/libs/login-api-2/src/main/res/values-th/strings.xml index 5ca4798884..c0d1d0a7f6 100644 --- a/libs/login-api-2/src/main/res/values-th/strings.xml +++ b/libs/login-api-2/src/main/res/values-th/strings.xml @@ -110,8 +110,7 @@ school.instructure.com ลบผู้ใช้ก่อนหน้า ไม่พบโรงเรียนของคุณ ลองพิมพ์ URL เต็มของสถานศึกษา - กดเลือกที่นี่เพื่อรับความช่วยเหลือ - + กดเลือกที่นี่เพื่อขอความช่วยเหลือในการล็อกอิน ไม่มีการเชื่อมต่ออินเทอร์เน็ต การดำเนินการนี้ต้องมีการเชื่อมต่ออินเทอร์เน็ต ต้องระบุหัวเรื่องและรายละเอียดเพื่อส่งข้อเสนอแนะ diff --git a/libs/login-api-2/src/main/res/values-vi/strings.xml b/libs/login-api-2/src/main/res/values-vi/strings.xml index 0b1ef97420..2894b5036d 100644 --- a/libs/login-api-2/src/main/res/values-vi/strings.xml +++ b/libs/login-api-2/src/main/res/values-vi/strings.xml @@ -110,8 +110,7 @@ school.instructure.com Loại Bỏ Người Dùng Trước Không tìm được trường của bạn? Hãy thử nhập URL đầy đủ của trường - Nhấn vào đây để được trợ giúp. - + Nhấn vào đây để được trợ giúp đăng nhập. Không Có Kết Nối Internet Thao tác này bắt buộc phải có kết nối internet. Bắt buộc phải có tiêu đề và mô tả thì mới có thể nộp ý kiến phản hồi. diff --git a/libs/login-api-2/src/main/res/values-zh/strings.xml b/libs/login-api-2/src/main/res/values-zh/strings.xml index 871b422f6d..6c791c527b 100644 --- a/libs/login-api-2/src/main/res/values-zh/strings.xml +++ b/libs/login-api-2/src/main/res/values-zh/strings.xml @@ -109,8 +109,7 @@ school.instructure.com 删除上一个用户 找不到您的学校?尝试键入完整的学校 URL。 - 轻击此处获取帮助。 - + 点击此处获取登录帮助。 无互联网连接 此操作需要互联网连接。 须填写主题和描述后才能提交反馈。 diff --git a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt index 4f5a8ad474..7b7909c6e6 100644 --- a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt +++ b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt @@ -34,7 +34,7 @@ enum class Priority { enum class FeatureCategory { ASSIGNMENTS, SUBMISSIONS, LOGIN, COURSE, DASHBOARD, GROUPS, SETTINGS, PAGES, DISCUSSIONS, MODULES, INBOX, GRADES, FILES, EVENTS, PEOPLE, CONFERENCES, COLLABORATIONS, SYLLABUS, TODOS, QUIZZES, NOTIFICATIONS, - ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS + ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS, OFFLINE_CONTENT } enum class SecondaryFeatureCategory { diff --git a/libs/pandares/src/main/res/drawable/ic_navigation_arc.xml b/libs/pandares/src/main/res/drawable/ic_navigation_arc.xml deleted file mode 100644 index 1b84e5686e..0000000000 --- a/libs/pandares/src/main/res/drawable/ic_navigation_arc.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index 6aefe0cf01..d99ab6d62a 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -1462,4 +1462,85 @@ البريد الإلكتروني الإصدار حدثت مشكلة أثناء إعادة تحميل هذه المهمة. يرجى التحقق من الاتصال وإعادة المحاولة. + شعار Instructure + التفضيلات + المحتوى دون اتصال + المزامنة + + + المحتوى دون اتصال + إدارة المحتوى دون اتصال + سعة التخزين + تم استخدام %s من %s + تطبيقات أخرى + Canvas Student + المتبقي + جميع المساقات + مزامنة + تم تحديد %d + تحديد الكل + إلغاء تحديد الكل + حدث خطأ أثناء تحميل المحتوى. + سيؤدي تمكين مزامنة المحتوى التلقائية إلى تنزيل المحتوى المحدد بناءً على الإعدادات أدناه. ستحدث مزامنة المحتوى حتى لو لم يكن يعمل التطبيق. إذا تم إيقاف تشغيل الإعداد، فلن تحدث المزامنة. لن يتم حذف المحتوى الذي تم تنزيله بالفعل. + تكرار المزامنة + مزامنة المحتوى التلقائية + حدد تكرار مزامنة المحتوى. سيقوم النظام بتنزيل المحتوى المحدد بناءً على التكرار المحدد هنا. + مزامنة المحتوى عبر شبكة Wi-Fi فقط + إذا تم تمكين هذا الإعداد، فلن تتم مزامنة المحتوى إلا إذا اتصل الجهاز بشبكة Wi-Fi، وإلا إذا تم تأجيل المزامنة حتى تتوفر شبكة Wi-Fi. + المزامنة + يوميًا + أسبوعيًا + تكرار المزامنة + هل تريد إيقاف تشغيل مزامنة المحتوى على Wi-fi فقط؟ + إذا تم تمكين هذا الإعداد، فلن تتم مزامنة المحتوى إلا إذا اتصل الجهاز بشبكة Wi-Fi، وإلا إذا تم تأجيل المزامنة حتى تتوفر شبكة Wi-Fi. + إيقاف التشغيل + يدوي + وضع عدم الاتصال + غير متاح دون اتصال + هذا المحتوى غير متاح في وضع عدم الاتصال. + هذا المحتوى غير متاح في وضع عدم الاتصال. إذا أردت تغيير إعداداتك، افتح شاشة المحتوى دون اتصال من لوحة المعلومات عند توفر الشبكة. + غير متصل + تعذرت المزامنة + جارٍ تنزيل %1$s من %2$s + في قائمة الانتظار + اكتملت مزامنة المحتوى دون اتصال + تعذرت مزامنة المحتوى دون اتصال + هل تريد إلغاء المزامنة؟ + سيؤدي إلى إيقاف مزامنة المحتوى دون اتصال. يمكنك القيام بهذا مرة أخرى لاحقًا. + تعذرت مزامنة ملف واحد أو أكثر. تحقق من اتصالك بالإنترنت وأعد محاولة الإرسال. + جارٍ بدء التنزيل + لا يمكن إضافة مساقات إلى المفضلة دون اتصال. + جميع المساقات + المساقات + المجموعات + جميع المساقات + لا يمكن تنفيذ تحديد المساقات للوحة المعلومات إلا عبر الاتصال بالإنترنت. يمكنك التنقل إلى تفاصيل المساق دون اتصال. + الملاحظة + نجحت العملية! تم تنزيل %1$s من %2$s + جارٍ مزامنة المحتوى دون اتصال + تجاهل الإعلام + + %d من المساقات قيد المزامنة. + %d مساق قيد المزامنة. + %d من المساقات قيد المزامنة. + %d من المساقات قيد المزامنة. + %d من المساقات قيد المزامنة. + %d من المساقات قيد المزامنة. + + صور محتوى المساق + هذه المهمة لم تعد متوفرة. + أنت غير متصل بالإنترنت + ليس لديك حاليًا أي مساقات غير متوفرة عبر الإنترنت. + نجحت مزامنة المحتوى دون اتصال + تعذرت مزامنة المحتوى دون اتصال + تحديثات المزامنة دون اتصال + إعلامات Canvas لتحديثات المزامنة دون اتصال. + + تمت مزامنة %d من المساقات. + تمت مزامنة %d مساق. + تمت مزامنة %d من المساقات. + تمت مزامنة %d من المساقات. + تمت مزامنة %d من المساقات. + تمت مزامنة %d من المساقات. + diff --git a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml index 3d89c5ace5..692494e0bb 100644 --- a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml @@ -1391,4 +1391,77 @@ E-mail Version Der opstod et problem med at genindlæse denne opgave. Kontrollér forbindelsen, og prøv igen. + Instructure-logo + Indstillinger + Offline indhold + Synkronisering + + + Offline indhold + Administrer offline indhold + Opbevaring + %s af %s brugt + Andre apps + Canvas-elev + Tilbage + Alle fag + Synkroniser + %d valgt + Vælg alle + Fravælg alle + Der opstod en fejl under indlæsning af indholdet. + Aktivering af automatisk indholdssynkronisering sørger for at downloade det valgte indhold baseret på nedenstående indstillinger. Indholdssynkroniseringen gennemføres, selvom applikationen ikke kører. Hvis indstillingen er deaktiveret, sker der ingen synkronisering. Det allerede downloadede indhold vil ikke blive slettet. + Synkroniseringsfrekvens + Automatisk indholdssynkronisering + Angiv gentagelsen af indholdssynkroniseringen. Systemet vil downloade det valgte indhold baseret på den frekvens, der angives her. + Synkroniser kun indhold via wi-fi + Hvis denne indstilling er aktiveret, vil indholdssynkroniseringen kun gennemføres, hvis enheden opretter forbindelse til et wi-fi-netværk, ellers vil det blive udskudt, indtil et wi-fi-netværk er tilgængeligt. + Synkronisering + Dagligt + Ugentlig + Synkroniseringsfrekvens + Slå indholdssynkronisering kun via wi-fi fra? + Hvis denne indstilling er aktiveret, vil indholdssynkroniseringen kun gennemføres, hvis enheden opretter forbindelse til et wi-fi-netværk, ellers vil det blive udskudt, indtil et wi-fi-netværk er tilgængeligt. + Sluk + Manuel + Offlinetilstand + Ikke tilgængelig offline + Dette indhold er ikke tilgængeligt i offlinetilstand. + Dette indhold er ikke tilgængeligt i offlinetilstand. Hvis du vil ændre dine indstillinger, skal du åbne skærmen Offlineindhold fra oversigten, når netværket er tilgængeligt. + Offline + Synkronisering mislykkedes + Downloader %1$s af %2$s + Sat i kø + Synkronisering af offlineindhold er fuldført + Synkronisering af offlineindhold mislykkedes + Vil du annullere synkronisering? + Det vil stoppe synkronisering af offlineindhold. Du kan gøre det igen senere. + En eller flere filer kunne ikke synkronisere. Kontroller din internetforbindelse, og prøv igen for at aflevere. + Download starter + Fag kan ikke føjes til favoritter offline. + Alle fag + Fag + Grupper + Alle fag + Valg af fag til oversigten kan kun ske online. Du kan navigere til offline fagdetaljer. + Bemærk + Succes! Downloadet %1$s af %2$s + Synkroniserer offline indhold + Afvis meddelelse + + %d faget synkroniseres. + %d fag synkroniseres. + + Fagindhold billeder + Denne opgave er ikke længere tilgængelig. + Du er offline + Du har i øjeblikket ingen fag, der er tilgængelige offline. + Offline indholdssynkronisering lykkedes + Synkronisering af offlineindhold mislykkedes + Offline synkroniseringsopdateringer + Canvas-meddelelser for offline synkroniseringsopdateringer. + + %d faget er blevet synkroniseret. + %d fagene er blevet synkroniseret. + diff --git a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml index 013c458795..c2f7ac0a04 100644 --- a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -1391,4 +1391,77 @@ Email Version There was a problem reloading this assignment. Please check your connection and try again. + Instructure logo + Preferences + Offline Content + Synchronisation + + + Offline Content + Manage Offline Content + Storage + %s of %s Used + Other Apps + Canvas Student + Remaining + All Subjects + Sync + %d Selected + Select All + Deselect All + An error occurred while loading the content. + Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronisation will happen even if the application is not running. If the setting is switched off, then no synchronisation will happen. The already downloaded content will not be deleted. + Sync Frequency + Auto Content Sync + Specify the recurrence of the content synchronisation. The system will download the selected content based on the frequency specified here. + Sync Content Over Wi-Fi Only + If this setting is enabled, the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Synchronisation + Daily + Weekly + Sync Frequency + Turn Off Content Sync Over Wi-fi Only? + If this setting is enabled, the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Turn Off + Manual + Offline Mode + Not Available Offline + This content is not available in offline mode. + This content is not available in offline mode. If you want to change your settings open the Offline Content screen from the dashboard when network is available. + Offline + Sync Failed + Downloading %1$s of %2$s + Queued + Offline Content Sync Completed + Offline Content Sync Failed + Cancel Sync? + It will stop offline content sync. You can do it again later. + One or more files failed to sync. Check your internet connection and retry to submit. + Download starting + Subjects cannot be added to favourites offline. + All Subjects + Subjects + Groups + All Subjects + Selecting subjects for Dashboard can only be done online. You can navigate to offline subject details. + Note + Success! Downloaded %1$s of %2$s + Syncing Offline Content + Dismiss notification + + %d subject is syncing. + %d subjects are syncing. + + Subject content images + This assignment is no longer available. + You are offline + You currently don\'t have any subjects that are available offline. + Offline Content Sync Success + Offline Content Sync Failed + Offline Sync updates + Canvas notifications for offline sync updates. + + %d subject has been synced. + %d subjects have been synced. + diff --git a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml index e860a4f857..9a7a75afe0 100644 --- a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -1391,4 +1391,77 @@ Email Version There was a problem reloading this assignment. Please check your connection and try again. + Instructure logo + Preferences + Offline Content + Synchronisation + + + Offline Content + Manage Offline Content + Storage + %s of %s Used + Other Apps + Canvas student + Remaining + All modules + Sync + %d Selected + Select all + Un-select All + An error occurred while loading the content. + Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. + Sync Frequency + Auto Content Sync + Specify the recurrence of the content synchronisation. The system will download the selected content based on the frequency specified here. + Sync Content Over Wi-Fi Only + If this setting is enabled the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Synchronisation + Daily + Weekly + Sync Frequency + Turn Off Content Sync Over Wi-fi Only? + If this setting is enabled the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Turn Off + Manual + Offline Mode + Not Available Offline + This content is not available in offline mode. + This content is not available in offline mode. If you want to change your settings open the Offline Content screen from the dashboard when network is available. + Offline + Sync Failed + Downloading %1$s of %2$s + Queued + Offline Content Sync Completed + Offline Content Sync Failed + Cancel Sync? + It will stop offline content sync. You can do it again later. + One or more files failed to sync. Check your internet connection and retry to submit. + Download starting + Modules cannot be added to favorites offline. + All modules + Modules + Groups + All modules + Selecting modules for Dashboard can only be done online. You can navigate to offline module details. + Note + Success! Downloaded %1$s of %2$s + Syncing Offline Content + Dismiss notification + + %d module is syncing. + %d modules are syncing. + + Module content images + This assignment is no longer available. + You are offline + You currently don\'t have any modules that are available offline. + Offline Content Sync Success + Offline Content Sync Failed + Offline Sync updates + Canvas notifications for offline sync updates. + + %d module has been synced. + %d modules have been synced. + diff --git a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml index 3bbbfdbd80..90a7760b69 100644 --- a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml @@ -1392,4 +1392,77 @@ E-post Versjon Det oppstod et problem med lasting av denne oppgaven. Kontroller tilkoblingen og prøv på nytt. + Instructure-logo + Preferanser + Frakoblet emneinnhold + Synkronisering + + + Frakoblet emneinnhold + Administrer frakoblet emneinnhold + Lagring + %s av %s brukte + Andre apper + Canvas-elev + Resterende + Alle åpne fag + Synkroniser + %d er valgt + Velg alle + Fjern all merking + Det oppsto en feil ved lasting av innholdet. + Aktivering av Automatisk synkronisering av innhold vil ta seg av nedlasting av det valgte innholdet basert på følgende innstillinger. Synkronisering av innhold vil skje selv om applikasjonen ikke kjører. Hvis innstillingen er slått av, vil det ikke skje noen synkronisering. Det allerede nedlastede innholdet vil ikke bli slettet. + Synkroniseringsfrekvens + Automatisk synkronisering av innhold + Spesifiser gjentakelsen av innholdssynkroniseringen. Systemet vil laste ned det valgte innholdet basert på frekvensen som er spesifisert her. + Synkroniser innhold kun over Wi-Fi + Hvis denne innstillingen er aktivert, vil innholdssynkroniseringen bare skje hvis enheten er koblet til et Wi-Fi-nettverk. Hvis ikke, vil det bli utsatt til et Wi-Fi-nettverk er tilgjengelig. + Synkronisering + Daglig + Ukentlig + Synkroniseringsfrekvens + Slå av innholdssynkronisering kun over Wi-Fi? + Hvis denne innstillingen er aktivert, vil innholdssynkroniseringen bare skje hvis enheten er koblet til et Wi-Fi-nettverk. Hvis ikke, vil det bli utsatt til et Wi-Fi-nettverk er tilgjengelig. + Slå av + Manuell + Frakoblet modus + Ikke tilgjengelig i frakoblet modus + Innholdet er ikke tilgjengelig i frakoblet modus. + Innholdet er ikke tilgjengelig i frakoblet modus. Hvis du vil endre innstillingene dine, åpne skjermen Frakoblet faginnhold fra oversikten når du er koblet til internett. + Frakoblet + Synkronisering mislyktes + Laster ned %1$s av %2$s + Satt i kø + Synkronisering av frakoblet emneinnhold fullført + Synkronisering av frakoblet emneinnhold mislyktes + Avbryte synkronisering? + Det vil stoppe synkronisering av frakoblet emneinnhold Du kan gjøre det igjen senere. + Én eller flere filer kunne ikke synkroniseres. Sjekk internettforbindelsen din og prøv å lever på nytt. + Nedlasting startere + Fag kan ikke legges til i favoritter i frakoblet modus. + Alle åpne fag + Fag + Grupper + Alle åpne fag + Å velge fag for oversikt kan bare gjøres når du er tilkoblet. Du kan navigere til fagdetaljer i frakoblet modus. + Merknad + Vellykket! Lastet ned %1$s av %2$s + Synkroniserer frakoblet innhold + Avvis varsling + + %d fag synkroniseres. + %d fag synkroniseres. + + Faginnhold-bilder + Denne oppgaven er ikke lenger tilgjengelig. + Du er frakoblet + Du har ingen fag som er tilgjengelig i frakoblet modus. + Synkronisering av frakoblet emneinnhold vellykket + Synkronisering av frakoblet emneinnhold mislyktes + Synkronisering av oppdateringer i frakoblet modus + Canvas-varslinger for synkronisering av oppdateringer i frakoblet modus. + + %d fag er synkronisert. + %d fag er synkronisert. + diff --git a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml index eb7b807e44..63545c2531 100644 --- a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml @@ -1391,4 +1391,77 @@ E-post Version Det uppstod ett problem när den här uppgiften skulle laddas om. Kontrollera din anslutning och försök igen. + Instructure-logotyp + Inställningar + Offlineinnehåll + Synkronisering + + + Offlineinnehåll + Hantera offlineinnehåll + Lagring + %s av %s Använda + Andra appar + Canvas-elev + Återstående + Alla kurser + Synkronisera + %d Vald + Välj alla + Avmarkera alla + Ett fel uppstod vid inläsning av innehållet. + Om automatisk innehållssynkronisering aktiveras laddas det valda innehållet ned baserat på nedanstående inställningar. Innehållssynkroniseringen sker även om programmet inte körs. Om inställningen är avstängd sker ingen synkronisering. Innehåll som redan laddats ned raderas inte. + Synkroniseringsfrekvens + Automatisk innehållssynkronisering + Ange hur ofta innehållssynkroniseringen ska ske. Systemet kommer att ladda ned det valda innehållet baserat på den frekvens du anger här. + Synkronisera endast innehåll över Wi-Fi + Om den här inställningen aktiveras sker innehållssynkroniseringen endast om enheten ansluter till ett Wi-Fi-nätverk, i annat fall skjuts den upp tills ett Wi-Fi-nätverk är tillgängligt. + Synkronisering + Varje dag + Veckovis + Synkroniseringsfrekvens + Stäng av Synkronisera endast innehåll över Wi-Fi? + Om den här inställningen aktiveras sker innehållssynkroniseringen endast om enheten ansluter till ett Wi-Fi-nätverk, i annat fall skjuts den upp tills ett Wi-Fi-nätverk är tillgängligt. + Stäng av + Manuell + Offlineläge + Inte tillgänglig offline + Det här innehållet är inte tillgängligt i offlineläge. + Det här innehållet är inte tillgängligt i offlineläge. Om du vill ändra dina inställningar öppnar du skärmen Offlineinnehåll i översikten när nätverket är tillgängligt. + Offline + Synkroniseringen misslyckades + Laddar ned %1$s av %2$s + I kö + Innehållssynkronisering offline slutfördes + Innehållssynkronisering offline misslyckades + Avbryta synkroniseringen? + Det stoppar synkronisering av offlineinnehåll. Du kan göra detta vid ett senare tillfälle. + En eller fler filer synkroniserades inte. Kontrollera din internetanslutning och försök lämna in igen. + Nedladdningen startar + Det går inte att lägga till kurser i favoriter offline. + Alla kurser + Kurser + Grupper + Alla kurser + Du kan endast välja kurser till översikten online. Du kan navigera till information om offlinekurser. + Anteckning + Framgång! Laddade ned %1$s av %2$s + Synkroniserar offlineinnehåll + Avvisa aviseringen + + %d-kurs synkroniserar. + %d-kurser synkroniserar. + + Bilder i kursinnehållet + Denna uppgift är inte längre tillgänglig. + Du är offline + Du har för närvarande inte några kurser som är tillgängliga offline. + Offlineinnehåll har synkroniserats + Innehållssynkronisering offline misslyckades + Uppdateringar för offlinesynkronisering + Canvas-aviseringar för uppdateringar för offlinesynkronisering. + + %d-kurs har synkroniserats. + %d-kurser har synkroniserats. + diff --git a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml index ee91fbdcd0..1c0a7022e7 100644 --- a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml @@ -1373,4 +1373,75 @@ 電郵 版本 重新載入此作業時發生問題。請檢查您的連接然後重試。 + Instructure 標誌 + 偏好設定 + 離線內容 + 同步化 + + + 離線內容 + 管理離線內容 + 儲存 + 使用 %s 的 %s + 其他應用程式 + Canvas Student + 其餘的 + 所有課程 + 同步 + 已選擇 %d + 全選 + 取消全選 + 載入內容時發生錯誤。 + 啟用「自動內容同步」將根據以下設定注意下載所選取的內容。內容同步將會進行,即使應用程式並未正在執行中。如果關閉設定,則將不會進行同步。已經下載的內容也將不會刪除。 + 同步頻率 + 自動內容同步 + 指定內容同步的重複進行。系統將根據此處指定的頻率下載所選取的內容。 + 僅透過 Wi-Fi 同步內容 + 如果啟用此設定,內容同步僅會在裝置連線至 Wi-Fi 網路時進行,否則將會延期至 Wi-Fi 網路可用為止。 + 同步化 + 每天 + 每週 + 同步頻率 + 關閉僅透過 Wi-Fi 同步內容? + 如果啟用此設定,內容同步僅會在裝置連線至 Wi-Fi 網路時進行,否則將會延期至 Wi-Fi 網路可用為止。 + 關閉 + 手動 + 離線模式 + 無法使用離線 + 此內容無法在離線模式中使用。 + 此內容無法在離線模式中使用。如果您想變更設定,請在網路可用時從控制面板開啟離線內容畫面。 + 離線 + 同步失敗 + 下載 %2$s 的 %1$s + 排隊 + 離線內容同步完成 + 離線內容同步失敗 + 取消同步? + 系統將停止離線內容同步。您可以稍後再進行。 + 無法同步一個或多個檔案。檢查您的網際網路連線並重試提交。 + 下載開始 + 離線課程無法添加到最愛。 + 所有課程 + 課程 + 群組 + 所有課程 + 為控制面板選取課程只能在線上完成。您可以導航到離線課程詳細資料。 + 注釋 + 成功!下載 %2$s 的 %1$s + 同步離線內容 + 解除通知 + + %d 課程正在同步中。 + + 課程內容影像 + 此作業不再可用。 + 您已離線 + 您目前沒有任何可離線使用的課程。 + 離線內容同步成功 + 離線內容同步失敗 + 離線同步更新 + 使用於離線同步更新的 Canvas 通知。 + + %d 課程已同步。 + diff --git a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml index 9fd0ff9c21..f9e4a98069 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml @@ -1373,4 +1373,75 @@ 电子邮件 版本 重新加载此作业时出错。请检查连接,然后重试。 + Instructure 徽标 + 首选项 + 离线内容 + 同步 + + + 离线内容 + 管理离线内容 + 存储空间 + %s/%s 个已使用 + 其他应用程序 + Canvas 学生 + 剩余 + 所有课程 + 同步 + %d 已选择 + 全选 + 取消全选 + 加载内容时出错。 + 如果启用“自动同步内容”,将根据以下设置下载所选内容。即使应用程序没有运行,内容也会同步。如果关闭该设置,将不会同步。已下载的内容不会被删除。 + 同步周期 + 自动同步内容 + 指定内容同步的周期。系统将根据此处指定的周期下载所选内容。 + 仅通过无线网络同步内容 + 如果启用该设置,将只在设备连接无线网络的情况下同步内容,否则将推迟到连接无线网络后再同步。 + 同步 + 每天 + 每周 + 同步周期 + 是否关闭仅通过无线网络同步内容? + 如果启用该设置,将只在设备连接无线网络的情况下同步内容,否则将推迟到连接无线网络后再同步。 + 关闭 + 手动 + 离线模式 + 离线时不可用 + 此内容不能在离线模式下使用。 + 此内容不能在离线模式下使用。如需更改设置,请在连接网络后从控制面板打开离线内容屏幕。 + 离线 + 同步失败 + 正在下载 %1$s/%2$s 项 + 已加入队列 + 离线内容同步已完成 + 离线同步内容失败 + 是否取消同步? + 将停止离线同步内容。您可以稍后再次操作。 + 一个或多个文件未能同步。请检查网络连接,并再次尝试提交。 + 正在开始下载 + 无法离线将课程添加到收藏 + 所有课程 + 课程 + 小组 + 所有课程 + 控制面板选择课程只能在离线模式下进行。您可以导航到离线课程详情。 + + 成功!已下载 %1$s/%2$s 项 + 正在同步脱机内容 + 解散通知 + + %d 门课程正在同步。 + + 课程内容图像 + 此作业不再可用。 + 您已离线 + 您目前没有任何可离线使用的课程。 + 离线同步内容成功 + 离线同步内容失败 + 离线同步更新 + Canvas 离线同步更新通知。 + + %d 门课程已同步。 + diff --git a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml index ee91fbdcd0..1c0a7022e7 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml @@ -1373,4 +1373,75 @@ 電郵 版本 重新載入此作業時發生問題。請檢查您的連接然後重試。 + Instructure 標誌 + 偏好設定 + 離線內容 + 同步化 + + + 離線內容 + 管理離線內容 + 儲存 + 使用 %s 的 %s + 其他應用程式 + Canvas Student + 其餘的 + 所有課程 + 同步 + 已選擇 %d + 全選 + 取消全選 + 載入內容時發生錯誤。 + 啟用「自動內容同步」將根據以下設定注意下載所選取的內容。內容同步將會進行,即使應用程式並未正在執行中。如果關閉設定,則將不會進行同步。已經下載的內容也將不會刪除。 + 同步頻率 + 自動內容同步 + 指定內容同步的重複進行。系統將根據此處指定的頻率下載所選取的內容。 + 僅透過 Wi-Fi 同步內容 + 如果啟用此設定,內容同步僅會在裝置連線至 Wi-Fi 網路時進行,否則將會延期至 Wi-Fi 網路可用為止。 + 同步化 + 每天 + 每週 + 同步頻率 + 關閉僅透過 Wi-Fi 同步內容? + 如果啟用此設定,內容同步僅會在裝置連線至 Wi-Fi 網路時進行,否則將會延期至 Wi-Fi 網路可用為止。 + 關閉 + 手動 + 離線模式 + 無法使用離線 + 此內容無法在離線模式中使用。 + 此內容無法在離線模式中使用。如果您想變更設定,請在網路可用時從控制面板開啟離線內容畫面。 + 離線 + 同步失敗 + 下載 %2$s 的 %1$s + 排隊 + 離線內容同步完成 + 離線內容同步失敗 + 取消同步? + 系統將停止離線內容同步。您可以稍後再進行。 + 無法同步一個或多個檔案。檢查您的網際網路連線並重試提交。 + 下載開始 + 離線課程無法添加到最愛。 + 所有課程 + 課程 + 群組 + 所有課程 + 為控制面板選取課程只能在線上完成。您可以導航到離線課程詳細資料。 + 注釋 + 成功!下載 %2$s 的 %1$s + 同步離線內容 + 解除通知 + + %d 課程正在同步中。 + + 課程內容影像 + 此作業不再可用。 + 您已離線 + 您目前沒有任何可離線使用的課程。 + 離線內容同步成功 + 離線內容同步失敗 + 離線同步更新 + 使用於離線同步更新的 Canvas 通知。 + + %d 課程已同步。 + diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml index 013976b360..bcb976cfa4 100644 --- a/libs/pandares/src/main/res/values-ca/strings.xml +++ b/libs/pandares/src/main/res/values-ca/strings.xml @@ -571,7 +571,7 @@ Col·laboracions Conferències Xat - Resultats + Competències Ves a la pàgina: @@ -1392,4 +1392,77 @@ Correu electrònic Versió Hi ha hagut un problema en tornar a carregar aquesta activitat. Reviseu la connexió i torneu-ho a provar. + Logotip de l’Instructure + Preferències + Contingut sense connexió + Sincronització + + + Contingut sense connexió + Gestiona el contingut sense connexió + Emmagatzematge + S’han utilitzat %s de %s + Altres aplicacions + Canvas Student + Restant + Totes les assignatures + Sincronitza + S’ha seleccionat %d + Selecciona-ho tot + Anul·la la selecció de tot + S\'ha produït un error en carregar el contingut. + En activar la sincronització automàtica del contingut es baixarà el contingut seleccionat segons les opcions de configuració indicades a continuació. Se sincronitzarà el contingut encara que l’aplicació no s’estigui executant. Si el paràmetre està desactivat no es durà a terme la sincronització. No se suprimirà el contingut que ja s’hagi baixat. + Freqüència de sincronització + Sincronització automàtica del contingut + Especifiqueu la recurrència de la sincronització del contingut. El sistema baixarà el contingut seleccionat segons la freqüència especificada en aquest apartat. + Sincronitzeu el contingut només a través de Wi-Fi + Si aquest paràmetre està activat, només se sincronitzarà el contingut si el dispositiu es connecta a una xarxa Wi-Fi; en cas contrari, l’acció es posposarà fins que hi hagi una xarxa Wi-Fi disponible. + Sincronització + Diari + Setmanal + Freqüència de sincronització + Voleu desactivar la sincronització del contingut a través de Wi-Fi? + Si aquest paràmetre està activat, només se sincronitzarà el contingut si el dispositiu es connecta a una xarxa Wi-Fi; en cas contrari, l’acció es posposarà fins que hi hagi una xarxa Wi-Fi disponible. + Desactiva + Manual + Mode sense connexió + No està disponible sense connexió + Aquest contingut no està disponible en mode sense connexió. + Aquest contingut no està disponible en mode sense connexió. Si voleu canviar la configuració, obriu la pantalla Contingut sense connexió des del panell de control quan la xarxa estigui disponible. + Sense connexió + No s’ha pogut dur a terme la sincronització + S\'estan baixant %1$s de %2$s + En cua + S’ha completat la sincronització del contingut sense connexió + No s’ha pogut sincronitzar el contingut sense connexió + Voleu cancel·lar la sincronització? + Amb aquesta acció s’aturarà la sincronització del contingut sense connexió. Podeu tornar-ho a fer més endavant. + No s\'han pogut sincronitzar un o més fitxers. Reviseu la connexió a Internet i torneu a provar de fer l\'entrega. + S’està iniciant la baixada + No es poden afegir les assignatures a Preferits sense connexió. + Totes les assignatures + Assignatures + Grups + Totes les assignatures + Seleccionar assignatures per al panell de control només es pot fer sense connexió. Podeu navegar a la informació de l’assignatura sense connexió. + Nota + Operació correcta! S’han baixat %1$s de %2$s + S’està sincronitzant el contingut sense connexió + Rebutgeu la notificació + + S’està sincronitzant %d assignatura. + S’estan sincronitzant %d assignatures. + + Imatges del contingut de l\'assignatura + Aquesta activitat ja no està disponible. + Esteu sense connexió + Actualment, no teniu cap assignatura que estigui disponible sense connexió. + S’ha sincronitzat correctament el contingut sense connexió + No s’ha pogut sincronitzar el contingut sense connexió + Actualitzacions de la sincronització sense connexió + Notificacions del Canvas d’actualitzacions de sincronització sense connexió. + + S’ha sincronitzat %d assignatura. + S’han sincronitzat %d assignatures. + diff --git a/libs/pandares/src/main/res/values-cy/strings.xml b/libs/pandares/src/main/res/values-cy/strings.xml index 8498f8bf6d..84e4e1f225 100644 --- a/libs/pandares/src/main/res/values-cy/strings.xml +++ b/libs/pandares/src/main/res/values-cy/strings.xml @@ -1391,4 +1391,77 @@ E-bost Fersiwn Problem wrth ail-lwytho’r aseiniad hwn. Gwiriwch eich cysylltiad a rhowch gynnig arall arni. + Logo Instructure + Blaenoriaethau + Cynnwys All-lein + Cysoni + + + Cynnwys All-lein + Rheoli Cynnwys All-lein + Storio + %s o %s wedi’i ddefnyddio + Apiau Eraill + Myfyriwr Canvas + Yn Weddill + Pob Cwrs + Cysoni + %d Wedi dewis + Dewis y cyfan + Dad-ddewis y Cyfan + Gwall wrth lwytho’r cynnwys. + Bydd galluogi Cysoni Cynnwys Awtomatig yn gofalu am lwytho’r cynnwys sydd wedi’i ddewis i lawr yn seiliedig ar y gosodiadau isod. Bydd cysoni cynnwys yn digwydd hyd yn oed os nad yw’r rhaglen yn rhedeg. Os yw’r gosodiadau wedi’i ddiffodd ni fydd cysoni’n digwydd. Ni fydd cynnwys sydd eisoes wedi’i lwytho i lawr yn cael ei ddileu. + Amlder Cysoni + Cysoni Cynnwys Awtomatig + Nodi dychweliad cysoni cynnwys. Bydd y system yn llwytho\'r cynnwys sydd wedi’i ddewis i lawn yn seiliedig ar yr amlder sydd wedi’i nodi yma. + Cysoni Cynnwys dros Wi-Fi yn unig + Os ydy’r gosodiad hwn wedi’i alluogi dim ond pan fydd y ddyfais wedi’i chysylltu i rwydwaith Wi-Fi y bydd cysoni cynnwys yn digwydd, fel arall bydd yn cael ei ohirio nes bod rhwydwaith Wi-Fi ar gael. + Cysoni + Pob dydd + Pob wythnos + Amlder Cysoni + Diffodd Cysoni Cynnwys Dros Wi-Fi yn Unig? + Os ydy’r gosodiad hwn wedi’i alluogi dim ond pan fydd y ddyfais wedi’i chysylltu i rwydwaith Wi-Fi y bydd cysoni cynnwys yn digwydd, fel arall bydd yn cael ei ohirio nes bod rhwydwaith Wi-Fi ar gael. + Diffodd + Llawlyfr + Modd All-lein + Ddim Ar Gael All-lein + Nid yw’r cynnwys hwn ar gael yn y modd all-lein. + Nid yw’r cynnwys hwn ar gael yn y modd all-lein. Os ydych chi eisiau newid eich gosodiadau agorwch y sgrin Cynnwys All-lein o’r dangosfwrdd pan mae’r rhwydwaith ar gael. + All-lein + Wedi methu cysoni + Wrthi’n llwytho i lawr %1$s o %2$s + Mewn ciw + Cysoni Cynnwys All-lein wedi’i Gwblhau + Cysoni Cynnwys All-lein wedi Methu + Canslo Cysoni? + Bydd y stopio cysoni cynnwys all-lein. Gallwch chi ei wneud eto yn nes ymlaen. + Wedi methu cysoni un neu ragor o ffeiliau. Gwiriwch eich cysylltiad rhyngrwyd a rhoi cynnig arall ar gyflwyno. + Yn dechrau llwytho i lawr + Does dim modd ychwanegu cyrsiau at ffefrynau all-lein. + Pob Cwrs + Cyrsiau + Grwpiau + Pob Cwrs + Dim ond ar-lein y mae modd dewis cyrsiau ar gyfer y Dangosfwrdd. Gallwch chi fynd i fanylion cwrs all-lein. + Nodyn + Wedi llwyddo! Wedi llwytho %1$s o %2$s i lawr + Cysoni Cynnwys All-lein + Diystyru hysbysu + + %d cwrs yn cysoni. + %d cwrs yn cysoni. + + Delweddau cynnwys cwrs + Dydy’r aseiniad hwn ddim ar gael mwyach. + Rydych chi all-lein + Ar hyn o bryd, does gennych chi ddim cyrsiau sydd ar gael all-lein. + Cysoni Cynnwys All-lein wedi Llwyddo + Cysoni Cynnwys All-lein wedi Methu + Diweddariadau Cysoni All-lein + Hysbysiadau Canvas ar gyfer diweddariadau cysoni all-lein + + %d cwrs wedi cael ei gysoni. + %d cwrs wedi cael eu cysoni. + diff --git a/libs/pandares/src/main/res/values-da/strings.xml b/libs/pandares/src/main/res/values-da/strings.xml index ddc3e5462f..9823813992 100644 --- a/libs/pandares/src/main/res/values-da/strings.xml +++ b/libs/pandares/src/main/res/values-da/strings.xml @@ -1391,4 +1391,77 @@ E-mail Version Der opstod et problem med at genindlæse denne opgave. Kontrollér forbindelsen, og prøv igen. + Instructure-logo + Indstillinger + Offline indhold + Synkronisering + + + Offline indhold + Administrer offline indhold + Opbevaring + %s af %s brugt + Andre apps + Canvas-studerende + Tilbage + Alle fag + Synkroniser + %d valgt + Vælg alle + Fravælg alle + Der opstod en fejl under indlæsning af indholdet. + Aktivering af automatisk indholdssynkronisering sørger for at downloade det valgte indhold baseret på nedenstående indstillinger. Indholdssynkroniseringen gennemføres, selvom applikationen ikke kører. Hvis indstillingen er deaktiveret, sker der ingen synkronisering. Det allerede downloadede indhold vil ikke blive slettet. + Synkroniseringsfrekvens + Automatisk indholdssynkronisering + Angiv gentagelsen af indholdssynkroniseringen. Systemet vil downloade det valgte indhold baseret på den frekvens, der angives her. + Synkroniser kun indhold via wi-fi + Hvis denne indstilling er aktiveret, vil indholdssynkroniseringen kun gennemføres, hvis enheden opretter forbindelse til et wi-fi-netværk, ellers vil det blive udskudt, indtil et wi-fi-netværk er tilgængeligt. + Synkronisering + Dagligt + Ugentlig + Synkroniseringsfrekvens + Slå indholdssynkronisering kun via wi-fi fra? + Hvis denne indstilling er aktiveret, vil indholdssynkroniseringen kun gennemføres, hvis enheden opretter forbindelse til et wi-fi-netværk, ellers vil det blive udskudt, indtil et wi-fi-netværk er tilgængeligt. + Sluk + Manuel + Offlinetilstand + Ikke tilgængelig offline + Dette indhold er ikke tilgængeligt i offlinetilstand. + Dette indhold er ikke tilgængeligt i offlinetilstand. Hvis du vil ændre dine indstillinger, skal du åbne skærmen Offlineindhold fra oversigten, når netværket er tilgængeligt. + Offline + Synkronisering mislykkedes + Downloader %1$s af %2$s + Sat i kø + Synkronisering af offlineindhold er fuldført + Synkronisering af offlineindhold mislykkedes + Vil du annullere synkronisering? + Det vil stoppe synkronisering af offlineindhold. Du kan gøre det igen senere. + En eller flere filer kunne ikke synkronisere. Kontroller din internetforbindelse, og prøv igen for at aflevere. + Download starter + Fag kan ikke føjes til favoritter offline. + Alle fag + Fag + Grupper + Alle fag + Valg af fag til oversigten kan kun ske online. Du kan navigere til offline fagdetaljer. + Bemærk + Succes! Downloadet %1$s af %2$s + Synkroniserer offline indhold + Afvis meddelelse + + %d faget synkroniseres. + %d fag synkroniseres. + + Fagindhold billeder + Denne opgave er ikke længere tilgængelig. + Du er offline + Du har i øjeblikket ingen fag, der er tilgængelige offline. + Offline indholdssynkronisering lykkedes + Synkronisering af offlineindhold mislykkedes + Offline synkroniseringsopdateringer + Canvas-meddelelser for offline synkroniseringsopdateringer. + + %d faget er blevet synkroniseret. + %d fagene er blevet synkroniseret. + diff --git a/libs/pandares/src/main/res/values-de/strings.xml b/libs/pandares/src/main/res/values-de/strings.xml index 58cda0e850..820eb976ff 100644 --- a/libs/pandares/src/main/res/values-de/strings.xml +++ b/libs/pandares/src/main/res/values-de/strings.xml @@ -1391,4 +1391,77 @@ E-Mail Version Beim erneuten Laden der Aufgabe ist ein Problem aufgetreten. Bitte überprüfen Sie Ihre Verbindung, und versuchen Sie es erneut. + Instructure-Logo + Präferenzen + Offline-Inhalte + Synchronisierung + + + Offline-Inhalte + Offline-Inhalte verwalten + Speicher + %s von %s verwendet + Andere Apps + Canvas-Studierende*r + Verbleibend + Alle Kurse + Synchronisieren + %d ausgewählt + Alle auswählen + Alle abwählen + Beim Laden des Inhalts ist ein Fehler aufgetreten. + Wenn Sie die automatische Inhaltssynchronisierung aktivieren, wird der Download der ausgewählten Inhalte auf der Grundlage der unten aufgeführten Einstellungen durchgeführt. Die Synchronisierung der Inhalte erfolgt auch dann, wenn die Anwendung nicht ausgeführt wird. Wenn die Einstellung ausgeschaltet ist, findet keine Synchronisierung statt. Die bereits heruntergeladenen Inhalte werden nicht gelöscht. + Synchronisierungsfrequenz + Automatische Synchronisation von Inhalten + Legen Sie die Häufigkeit der Inhaltssynchronisierung fest. Das System lädt die ausgewählten Inhalte entsprechend der hier angegebenen Häufigkeit herunter. + Inhalte nur über WLAN synchronisieren + Wenn diese Einstellung aktiviert ist, erfolgt die Inhaltssynchronisierung nur, wenn das Gerät mit einem WLAN-Netzwerk verbunden ist. Andernfalls wird sie verschoben, bis ein WLAN-Netzwerk verfügbar ist. + Synchronisierung + Täglich + Wöchentlich + Synchronisierungsfrequenz + Inhaltssynchronisierung nur über Wi-Fi ausschalten? + Wenn diese Einstellung aktiviert ist, erfolgt die Inhaltssynchronisierung nur, wenn das Gerät mit einem WLAN-Netzwerk verbunden ist. Andernfalls wird sie verschoben, bis ein WLAN-Netzwerk verfügbar ist. + Ausschalten + Manuell + Offline-Modus + Offline nicht verfügbar + Dieser Inhalt ist im Offline-Modus nicht verfügbar. + Dieser Inhalt ist im Offline-Modus nicht verfügbar. Wenn Sie Ihre Einstellungen ändern möchten, öffnen Sie die Ansicht „Offline-Inhalte“ im Dashboard, wenn das Netzwerk verfügbar ist. + Offline + Synchronisation fehlgeschlagen + %1$s von %2$s wird heruntergeladen + Wartend + Synchronisierung von Offline-Inhalten abgeschlossen + Offline-Inhaltssynchronisierung fehlgeschlagen + Synchronisierung abbrechen? + Dies wird die Synchronisierung der Offline-Inhalte stoppen. Sie können dies später erneut durchführen. + Eine oder mehrere Dateien wurden nicht synchronisiert. Überprüfen Sie Ihre Internetverbindung, und versuchen Sie es erneut. + Download beginnt + Kurse können offline nicht zu den Favoriten hinzugefügt werden. + Alle Kurse + Kurse + Gruppen + Alle Kurse + Die Auswahl von Kursen für das Dashboard kann nur online erfolgen. Sie können zu den Offline-Kursdetails navigieren. + Anmerkung + Erfolg! %1$s von %2$s heruntergeladen + Offline-Inhalte werden synchronisiert + Benachrichtigung verwerfen + + %d Kurs wird synchronisiert. + %d Kurse werden synchronisiert. + + Bilder zum Kursinhalt + Diese Aufgabe ist nicht mehr verfügbar. + Sie sind offline + Sie haben derzeit keine offline verfügbaren Kurse. + Synchronisierung von Offline-Inhalten erfolgreich + Synchronisierung vpn Offline-Inhalten fehlgeschlagen + Updates zur Offline-Synchronisierung + Canvas-Benachrichtigungen zu Updates zur Offline-Synchronisierung + + %d Kurs wurde synchronisiert. + %d Kurse wurden synchronisiert. + diff --git a/libs/pandares/src/main/res/values-en-rAU/strings.xml b/libs/pandares/src/main/res/values-en-rAU/strings.xml index 8aa32c6d13..a94e856389 100644 --- a/libs/pandares/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml @@ -1391,4 +1391,77 @@ Email Version There was a problem reloading this assignment. Please check your connection and try again. + Instructure logo + Preferences + Offline Content + Synchronisation + + + Offline Content + Manage Offline Content + Storage + %s of %s Used + Other Apps + Canvas Student + Remaining + All Courses + Sync + %d Selected + Select All + Deselect All + An error occurred while loading the content. + Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronisation will happen even if the application is not running. If the setting is switched off, then no synchronisation will happen. The already downloaded content will not be deleted. + Sync Frequency + Auto Content Sync + Specify the recurrence of the content synchronisation. The system will download the selected content based on the frequency specified here. + Sync Content Over Wi-Fi Only + If this setting is enabled, the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Synchronisation + Daily + Weekly + Sync Frequency + Turn Off Content Sync Over Wi-fi Only? + If this setting is enabled, the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Turn Off + Manual + Offline Mode + Not Available Offline + This content is not available in offline mode. + This content is not available in offline mode. If you want to change your settings open the Offline Content screen from the dashboard when network is available. + Offline + Sync Failed + Downloading %1$s of %2$s + Queued + Offline Content Sync Completed + Offline Content Sync Failed + Cancel Sync? + It will stop offline content sync. You can do it again later. + One or more files failed to sync. Check your internet connection and retry to submit. + Download starting + Courses cannot be added to favourites offline. + All Courses + Courses + Groups + All Courses + Selecting courses for Dashboard can only be done online. You can navigate to offline course details. + Note + Success! Downloaded %1$s of %2$s + Syncing Offline Content + Dismiss notification + + %d course is syncing. + %d courses are syncing. + + Course content images + This assignment is no longer available. + You are offline + You currently don\'t have any courses that are available offline. + Offline Content Sync Success + Offline Content Sync Failed + Offline Sync updates + Canvas notifications for offline sync updates. + + %d course has been synced. + %d courses have been synced. + diff --git a/libs/pandares/src/main/res/values-en-rCY/strings.xml b/libs/pandares/src/main/res/values-en-rCY/strings.xml index e860a4f857..9a7a75afe0 100644 --- a/libs/pandares/src/main/res/values-en-rCY/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCY/strings.xml @@ -1391,4 +1391,77 @@ Email Version There was a problem reloading this assignment. Please check your connection and try again. + Instructure logo + Preferences + Offline Content + Synchronisation + + + Offline Content + Manage Offline Content + Storage + %s of %s Used + Other Apps + Canvas student + Remaining + All modules + Sync + %d Selected + Select all + Un-select All + An error occurred while loading the content. + Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. + Sync Frequency + Auto Content Sync + Specify the recurrence of the content synchronisation. The system will download the selected content based on the frequency specified here. + Sync Content Over Wi-Fi Only + If this setting is enabled the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Synchronisation + Daily + Weekly + Sync Frequency + Turn Off Content Sync Over Wi-fi Only? + If this setting is enabled the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Turn Off + Manual + Offline Mode + Not Available Offline + This content is not available in offline mode. + This content is not available in offline mode. If you want to change your settings open the Offline Content screen from the dashboard when network is available. + Offline + Sync Failed + Downloading %1$s of %2$s + Queued + Offline Content Sync Completed + Offline Content Sync Failed + Cancel Sync? + It will stop offline content sync. You can do it again later. + One or more files failed to sync. Check your internet connection and retry to submit. + Download starting + Modules cannot be added to favorites offline. + All modules + Modules + Groups + All modules + Selecting modules for Dashboard can only be done online. You can navigate to offline module details. + Note + Success! Downloaded %1$s of %2$s + Syncing Offline Content + Dismiss notification + + %d module is syncing. + %d modules are syncing. + + Module content images + This assignment is no longer available. + You are offline + You currently don\'t have any modules that are available offline. + Offline Content Sync Success + Offline Content Sync Failed + Offline Sync updates + Canvas notifications for offline sync updates. + + %d module has been synced. + %d modules have been synced. + diff --git a/libs/pandares/src/main/res/values-en-rGB/strings.xml b/libs/pandares/src/main/res/values-en-rGB/strings.xml index 70d3e97250..a838cfd2cd 100644 --- a/libs/pandares/src/main/res/values-en-rGB/strings.xml +++ b/libs/pandares/src/main/res/values-en-rGB/strings.xml @@ -1391,4 +1391,77 @@ Email Version There was a problem reloading this assignment. Please check your connection and try again. + Instructure logo + Preferences + Offline Content + Synchronisation + + + Offline Content + Manage Offline Content + Storage + %s of %s Used + Other Apps + Canvas student + Remaining + All courses + Sync + %d Selected + Select all + Un-select All + An error occurred while loading the content. + Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. + Sync Frequency + Auto Content Sync + Specify the recurrence of the content synchronisation. The system will download the selected content based on the frequency specified here. + Sync Content Over Wi-Fi Only + If this setting is enabled the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Synchronisation + Daily + Weekly + Sync Frequency + Turn Off Content Sync Over Wi-fi Only? + If this setting is enabled the content synchronisation will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Turn Off + Manual + Offline Mode + Not Available Offline + This content is not available in offline mode. + This content is not available in offline mode. If you want to change your settings open the Offline Content screen from the dashboard when network is available. + Offline + Sync Failed + Downloading %1$s of %2$s + Queued + Offline Content Sync Completed + Offline Content Sync Failed + Cancel Sync? + It will stop offline content sync. You can do it again later. + One or more files failed to sync. Check your internet connection and retry to submit. + Download starting + Courses cannot be added to favorites offline. + All courses + Courses + Groups + All courses + Selecting courses for Dashboard can only be done online. You can navigate to offline course details. + Note + Success! Downloaded %1$s of %2$s + Syncing Offline Content + Dismiss notification + + %d course is syncing. + %d courses are syncing. + + Course content images + This assignment is no longer available. + You are offline + You currently don\'t have any courses that are available offline. + Offline Content Sync Success + Offline Content Sync Failed + Offline Sync updates + Canvas notifications for offline sync updates. + + %d course has been synced. + %d courses have been synced. + diff --git a/libs/pandares/src/main/res/values-es-rES/strings.xml b/libs/pandares/src/main/res/values-es-rES/strings.xml index 8cef4f43c9..37d94a6525 100644 --- a/libs/pandares/src/main/res/values-es-rES/strings.xml +++ b/libs/pandares/src/main/res/values-es-rES/strings.xml @@ -1393,4 +1393,77 @@ Correo electrónico Versión Se ha producido un problema al volver a cargar esta actividad. Comprueba tu conexión y vuelve a intentarlo. + Logo de Instructure + Preferencias + Contenido sin conexión + Sincronización + + + Contenido sin conexión + Gestionar contenido sin conexión + Almacenamiento + %s de %s utilizado + Otras aplicaciones + Estudiante de Canvas + Restante + Todos los cursos + Sincronización + %d Seleccionado + Seleccionar todo + Deseleccionar todo + Ha habido un error al cargar el contenido. + Con la acción de habilitar la sincronización automática de contenidos, se descargará el contenido seleccionado según los siguientes ajustes. La sincronización del contenido se realizará incluso aunque la aplicación no esté en funcionamiento. Si los ajustes están apagados, no se realizará la sincronización. El contenido ya descargado no podrá eliminarse. + Frecuencia de sincronización + Sincronización automática del contenido + Especificar la recurrencia de la sincronización del contenido. El sistema descargará el contenido seleccionado basándose en la frecuencia aquí indicada. + Sincronizar contenido solo con wifi + Si se habilita está configuración, la sincronización del contenido solo se realizará si el dispositivo se conecta a una red wifi, de lo contrario se pospondrá hasta que haya una red wifi disponible. + Sincronización + Diariamente + Semanalmente + Frecuencia de sincronización + ¿Apagar la sincronización del contenido solo con wifi? + Si se habilita está configuración, la sincronización del contenido solo se realizará si el dispositivo se conecta a una red wifi, de lo contrario se pospondrá hasta que haya una red wifi disponible. + Apagar + Manual + Modo sin conexión + No disponible sin conexión + Este contenido no está disponible sin conexión. + Este contenido no está disponible sin conexión. Si quieres cambiar la configuración, abre la pantalla de contenido sin conexión en el panel de control cuando haya una conexión disponible. + Sin conexión + Sincronización fallida + Descargando %1$s de %2$s + En cola + Sincronización del contenido sin conexión completada + Ha habido un error en la sincronización del contenido sin conexión + ¿Cancelar sincronización? + Se parará la sincronización del contenido sin conexión. Lo puedes hacer de nuevo. + No se han podido sincronizar uno o más archivos. Comprueba tu conexión a Internet y vuelve a realizar la entrega. + Empezando la descarga + No se pueden añadir cursos a favoritos sin conexión. + Todos los cursos + Cursos + Grupos + Todos los cursos + Los cursos solo se pueden seleccionar del panel de control cuando hay conexión. Sin conexión se puede ver la información del curso. + Nota + Hecho Descargado %1$s de %2$s + Sincronizar contenido sin conexión + Descartar notificación + + %d curso se está sincronizando. + %d cursos se están sincronizando. + + Contenido de las imágenes del curso + Esta actividad ya no está disponible. + No tienes conexión + Actualmente no tienes ningún curso disponible sin conexión. + Sincronización del contenido sin conexión realizada + Ha habido un error en la sincronización del contenido sin conexión + Actualizaciones de sincronización sin conexión + Notificaciones de Canvas para actualizaciones sin conexión. + + %d curso se ha sincronizado. + %d cursos se han sincronizado. + diff --git a/libs/pandares/src/main/res/values-es/strings.xml b/libs/pandares/src/main/res/values-es/strings.xml index 1aaa81bfb7..71d1cdce1e 100644 --- a/libs/pandares/src/main/res/values-es/strings.xml +++ b/libs/pandares/src/main/res/values-es/strings.xml @@ -1391,4 +1391,77 @@ Correo electrónico Versión Se produjo un problema al volver a cargar esta tarea. Compruebe su conexión y vuelva a intentarlo. + Logotipo de Instructure + Preferencias + Contenido sin conexión + Sincronización + + + Contenido sin conexión + Administrar contenido sin conexión + Almacenamiento + %s de %s usado(s) + Otras aplicaciones + Estudiante de Canvas + Restante + Todos los cursos + Sincronización + %d seleccionado/a + Seleccionar todo + Desmarcar todo + Hubo un error al cargar el contenido. + Habilitar la sincronización automática de contenido se ocupará de descargar el contenido seleccionado en función de las configuraciones siguientes. La sincronización de contenido se realizará incluso si la aplicación no se está ejecutando. Si la configuración está desactivada, no se realizará ninguna sincronización. El contenido ya descargado no se eliminará. + Frecuencia de sincronización + Sincronización automática de contenido + Especifique la recurrencia de la sincronización de contenido. El sistema descargará el contenido seleccionado según la frecuencia especificada aquí. + Sincronizar contenido solo con Wi-Fi + Si esta configuración está habilitada, la sincronización de contenido solo se realizará si el dispositivo se conecta a una red Wi-Fi; de lo contrario, se postergará hasta que haya una red Wi-Fi disponible. + Sincronización + Diariamente + Semanalmente + Frecuencia de sincronización + ¿Desactivar la sincronización de contenido solo por Wi-Fi? + Si esta configuración está habilitada, la sincronización de contenido solo se realizará si el dispositivo se conecta a una red Wi-Fi; de lo contrario, se postergará hasta que haya una red Wi-Fi disponible. + Apagar + Manual + Modo sin conexión + No disponible sin conexión + Este contenido no está disponible en el modo sin conexión. + Este contenido no está disponible en el modo sin conexión. Si quiere cambiar las configuraciones, abra la pantalla de Contenido sin conexión desde el tablero cuando la red esté disponible. + Sin conexión + Error en la sincronización + Descargando %1$s de %2$s + En fila + Sincronización de contenido sin conexión completa + Error en la sincronización de contenido sin conexión + ¿Desea cancelar la sincronización? + Detendrá la sincronización de contenido sin conexión. Puede volver a hacerlo más tarde. + Error al sincronizar uno o más archivos. Compruebe su conexión a Internet y vuelva a entregarlos. + La descarga está comenzando + No se puede agregar cursos a favoritos sin conexión. + Todos los cursos + Cursos + Grupos + Todos los cursos + La selección de cursos desde el tablero solo se puede realizar con conexión. Puede navegar hacia los detalles del curso sin conexión. + Nota + ¡Éxito! Descargado %1$s de %2$s + Sincronizar contenido sin conexión. + Ignorar notificación. + + Se está sincronizando el curso %d. + Se están sincronizando los cursos %d. + + Imágenes del contenido del curso + Esta tarea ya no está disponible. + Está sin conexión + Actualmente, no tiene ningún curso disponible sin conexión. + Sincronización de contenido sin conexión exitosa + Error en la sincronización de contenido sin conexión + Actualizaciones de sincronización sin conexión + Notificaciones de Canvas para actualizaciones de sincronización sin conexión. + + Se ha sincronizado el curso %d. + Se han sincronizado los cursos %d. + diff --git a/libs/pandares/src/main/res/values-fi/strings.xml b/libs/pandares/src/main/res/values-fi/strings.xml index ab65479f50..2e783d8563 100644 --- a/libs/pandares/src/main/res/values-fi/strings.xml +++ b/libs/pandares/src/main/res/values-fi/strings.xml @@ -1391,4 +1391,77 @@ Sähköposti Versio Tämän tehtävän uudelleenlataamisessa ilmeni ongelma. Tarkasta yhteytesi ja yritä uudelleen. + Instructure-logo + Asetukset + Verkon ulkopuolinen sisältö + Synkronointi + + + Verkon ulkopuolinen sisältö + Hallitse verkon ulkopuolista sisältöä + Säilytystila + %s / %s käytetty + Muut sovellukset + Canvas-opiskelija + Jäljelle jäänyt + Kaikki kurssit + Synkronointi + %d Valittu: + Valitse kaikki + Poista kaikkien valinta + Kurssin sisältöä ladattaessa ilmeni virhe. + Automaattisen sisällön synkronisoinnin ottamisellal käyttöön huolehditaan valitun sisällön latauksesta alla olevien asetusten perusteella. Sisällön synkronisointi tapahtuu myös silloin, kun sovellus ei ole käynnissä jos asetukset on kytketty pois päältä, synkronisointia ei tapahdu. Jo ladattua sisältöä ei poisteta. + Synkronoinnin tiheys + Automaattinen sisällön synkronisointi + Määritä sisällön synkronisoinnin toistuminen. Järjestelmä lataa valitun sisällön täällä määritetyn taajuuden perusteella. + Synkronisoi sisältö vain Wifin kautta + Jos tämä asetus on käytössä, sisällön synkronisointi tapahtuu, jos laite on yhteydessä Wifi-verkkoon. Muussa tapauksessa tätä lykätään, kun Wifi-verkko on käytössä. + Synkronointi + Päivittäin + Viikoittainen + Synkronoinnin tiheys + Kytkentäänkö synkronointi vain Wi-Fin kautta pois päältä? + Jos tämä asetus on käytössä, sisällön synkronisointi tapahtuu, jos laite on yhteydessä Wifi-verkkoon. Muussa tapauksessa tätä lykätään, kun Wifi-verkko on käytössä. + Sammuta + Manuaalinen + Offline-tila + Ei ole saatavissa verkon ulkopuolella + Tämä toiminto ei ole saatavissa verkon ulkopuolella. + Tämä toiminto ei ole saatavissa verkon ulkopuolella. Jos haluat muuttaa asetuksiasi, avaa verkon ulkopuolisen sisällön näyttö koontinäytöltä, kun verkko on saatavissa. + Ei verkkoyhteyttä + Synkronointi ei onnistunut + Ladataan %1$s / %2$s + Jonotettu + Verkon ulkopuolisen sisällön synkronisointi valmis + Verkon ulkopuolisen sisällön synkronisointi epäonnistui. + Peruutetaanko synkronisointi? + Se keskeyttää verkon ulkopuolisen sisällön synkronoinnin. Voit tehdä sen uudelleen myöhemmin. + Yhden tai useamman tiedoston synkronisointi ei onnistunut. Tarkista Internet-yhteytesi ja yritä lähettää uudelleen. + Lataus alkaa + Kursseja ei voi lisätä suosikkeihin verkon ulkopuolisessa tilassa. + Kaikki kurssit + Kurssit + Ryhmät + Kaikki kurssit + Kursseja voi valita koontinäyttöön vain verkossa. Voit siirtyä verkon ulkopuolisen kurssin tietoihin. + Huomautus + Onnistui! Ladataan %1$s / %2$s + Verkon ulkopuolista sisältöä synkronoidaan + Ohita ilmoitus + + %d kurssia synkronoidaan. + %d kurssia synkronoidaan. + + Kurssin sisällön kuvakkeet. + Tämä tehtävä ei enää ole käytettävissä. + Olet verkon ulkopuolella + Sinulla ei parhaillaan ole kursseja, jotka ovat saatavilla verkon ulkopuolella. + Verkon ulkopuolisen sisällön synkronointi onnistui + Verkon ulkopuolisen sisällön synkronisointi epäonnistui. + Verkon ulkopuolise synkronoinnin päivitykset + Canvas-ilmoitukset sovelluksen verkon ulkopuolisille päivityksille. + + %d kurssi on synkronoitu. + %d kurssia on synkronoitu. + diff --git a/libs/pandares/src/main/res/values-fr-rCA/strings.xml b/libs/pandares/src/main/res/values-fr-rCA/strings.xml index dd583f2abc..72c52b2f56 100644 --- a/libs/pandares/src/main/res/values-fr-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-fr-rCA/strings.xml @@ -1391,4 +1391,77 @@ Adresse électronique Version Un problème est survenu lors du rechargement de cette tâche. Veuillez vérifier votre connexion et réessayer. + Logo d’Instructure + Préférences + Contenu hors ligne + Synchronisation + + + Contenu hors ligne + Gérer le contenu hors connexion + Stockage + %s de %s utilisé + Autres applications + Étudiant Canvas + Restant + Tous les cours + Synchronisation + %d sélectionné + Sélectionner tout + Désélectionner tout + Une erreur s’est produite lors du chargement du contenu. + L’activation de la synchronisation automatique du contenu se chargera de télécharger le contenu sélectionné en fonction des paramètres ci-dessous. La synchronisation du contenu se produit même si l’application n’est pas en cours d’exécution. Si le paramètre est désactivé, aucune synchronisation ne se produira. Le contenu déjà téléchargé ne sera pas supprimé. + Fréquence de synchronisation + Synchronisation automatique du contenu + Spécifiez la périodicité de la synchronisation de contenu. Le système téléchargera le contenu sélectionné en fonction de la fréquence spécifiée ici. + Synchroniser le contenu par Wi-Fi uniquement + Si ce paramètre est activé, la synchronisation du contenu ne se produira que si l’appareil se connecte à un réseau Wi-Fi, sinon elle sera reportée jusqu’à ce qu’un réseau Wi-Fi soit disponible. + Synchronisation + Tous les jours + Hebdomadaire + Fréquence de synchronisation + Désactiver la synchronisation du contenu sur Wi-fi uniquement? + Si ce paramètre est activé, la synchronisation du contenu ne se produira que si l’appareil se connecte à un réseau Wi-Fi, sinon elle sera reportée jusqu’à ce qu’un réseau Wi-Fi soit disponible. + Désactiver + Manuel + Mode hors ligne + Non disponible hors ligne + Ce contenu n’est pas disponible en mode hors ligne. + Ce contenu n’est pas disponible en mode hors ligne. Si vous souhaitez modifier vos paramètres, ouvrez l’écran « Contenu hors ligne » à partir du tableau de bord lorsque le réseau est disponible. + Hors ligne + Échec de la synchronisation + Téléchargement de %1$s de %2$s + En file d’attente + Synchronisation du contenu hors ligne terminée + Échec de la synchronisation du contenu hors connexion + Annuler la synchronisation? + Cela arrêtera la synchronisation du contenu hors ligne. Vous pourrez le refaire plus tard. + La synchronisation d’un ou plusieurs fichiers a échoué. Vérifiez votre connexion Internet et réessayez l’envoi. + Démarrage du téléchargement + Les cours ne peuvent pas être ajoutés aux favoris en étant hors ligne. + Tous les cours + Cours + Groupes + Tous les cours + La sélection des cours pour le tableau de bord ne peut se faire qu’en ligne. Vous pouvez accéder aux détails de la formation en étant hors ligne. + Remarque + Succès! Téléchargé %1$s de %2$s + Synchronisation hors connexion du contenu + Ignorer la notification + + Synchronisation de %d cours. + Synchronisation de %d cours. + + Images du contenu du cours + Cette tâche n’est plus disponible. + Vous êtes hors ligne + Vous n’avez actuellement aucun cours disponible hors ligne. + Succès de la synchronisation du contenu hors ligne + Échec de la synchronisation du contenu hors connexion + Mises à jour de synchronisation hors ligne + Notifications Canvas pour les mises à jour de synchronisation hors ligne. + + %d cours a été synchronisé. + %d cours ont été synchronisés. + diff --git a/libs/pandares/src/main/res/values-fr/strings.xml b/libs/pandares/src/main/res/values-fr/strings.xml index adaf919f50..b4f1328b77 100644 --- a/libs/pandares/src/main/res/values-fr/strings.xml +++ b/libs/pandares/src/main/res/values-fr/strings.xml @@ -1391,4 +1391,77 @@ Email Version Il y a eu un problème pour recharger ce travail. Veuillez vérifier votre connexion, puis réessayez. + Logo Instructure + Préférences + Contenu hors ligne + Synchronisation + + + Contenu hors ligne + Gérer le contenu hors ligne + Stockage + Utilisation de %s sur %s + Autres applications + Élève Canvas + Restant + Tous les cours + Synchro + %d Sélectionné + Tout sélectionner + Tout désélectionner + Une erreur est survenue lors du chargement du contenu. + L\'activation de la synchronisation auto du contenu gérera automatiquement le téléchargement du contenu sélectionné en fonction des paramètres ci-dessous. La synchronisation du contenu aura lieu même si l’application n’est pas en cours d’exécution. Si le paramètre est désactivé, aucune synchronisation n’aura lieu. Le contenu déjà téléchargé ne sera pas supprimé. + Fréquence de la synchronisation + Synchronisation auto du contenu + Indiquez la fréquence de la synchronisation du contenu. Le système téléchargera le contenu sélectionné selon la fréquence indiquée ici. + Synchronisation du contenu en Wi-Fi seulement + Si ce paramètre est activé, la synchronisation du contenu n\'aura lieu que si l’appareil est connecté à un réseau Wi-Fi. Dans le cas contraire, elle sera différée jusqu\'à ce qu\'un réseau Wi-Fi soit disponible. + Synchronisation + Tous les jours + Toutes les semaines + Fréquence de la synchronisation + Désactiver la synchronisation du contenu en Wi-Fi seulement ? + Si ce paramètre est activé, la synchronisation du contenu n\'aura lieu que si l’appareil est connecté à un réseau Wi-Fi. Dans le cas contraire, elle sera différée jusqu\'à ce qu\'un réseau Wi-Fi soit disponible. + Désactiver + Mode manuel + Mode hors ligne + Non disponible hors ligne + Ce contenu n’est pas disponible hors ligne. + Ce contenu n’est pas disponible hors ligne. Si vous souhaitez modifier vos paramètres, ouvrez l\'écran Contenu hors ligne à partir du tableau de bord lorsqu’un réseau est disponible. + Hors ligne + Échec de la synchronisation + Téléchargement de %1$s sur %2$s + En attente + Synchronisation du contenu hors ligne terminée + Échec de la synchronisation du contenu hors ligne + Annuler la synchronisation ? + Cette action interrompra la synchronisation de contenu hors ligne. Vous pourrez le faire plus tard. + Un ou plusieurs fichiers n’ont pas pu être synchronisés. Vérifiez l’état de votre connexion internet, puis réessayez. + Téléchargement commencé + Vous ne pouvez pas ajouter de cours favoris en mode hors ligne. + Tous les cours + Cours + Groupes + Tous les cours + La sélection de cours pour le tableau de bord ne peut s’effectuer qu’en mode en ligne. En mode hors ligne, vous pouvez toutefois naviguer jusqu’aux détails du cours. + Remarque + Réussite ! Téléchargement de %1$s sur %2$s + Synchronisation hors ligne du contenu + Rejeter Notification + + %d cours est en cours de synchronisation. + %d cours sont en cours de synchronisation. + + Images du contenu du cours + Ce travail n’est plus disponible. + Vous êtes hors ligne. + Vous n’avez actuellement aucun cours disponible hors ligne. + Synchronisation du contenu hors ligne réussie + Échec de la synchronisation du contenu hors ligne + Mises à jour des synchronisations hors ligne + Notifications Canvas de mise à jour des synchronisations hors ligne + + %d cours a été synchronisé. + %d cours ont été synchronisés. + diff --git a/libs/pandares/src/main/res/values-ht/strings.xml b/libs/pandares/src/main/res/values-ht/strings.xml index 4d9e1e3de6..5eda272465 100644 --- a/libs/pandares/src/main/res/values-ht/strings.xml +++ b/libs/pandares/src/main/res/values-ht/strings.xml @@ -1391,4 +1391,77 @@ Imèl Vèsyon Te gen yon pwoblèm pou rechaje devwa sa a. Tanpri verifye koneksyon ou a epi eseye ankò. + Logo Instructure + Preferans + Kontni San Koneksyon + Senkwonizasyon + + + Kontni San Koneksyon + Jere Kontni San Koneksyon + Estokaj + %s sou %s Itilize + Lòt Aplikasyon + Canvas Student + Rete + Tout Kou + Sync + %d Seleksyone + Seleksyone Tout + Deseleksyone Tout + Yon erè fèt pandan chajman kontni an. + Aktivasyon senkwonizasyon Kontni Otomatik la ap gen pou telechaje kontni ki seleksyone a an fonksyon de paramèt ki anba yo. Senkwonizasyon kontni an ap fèt menm si aplikasyon an pa ekzekite. Si reglaj la dezaktive, pa gen senkwonizasyon k ap fèt. Kontni ki deja telechaje a pa p efase. + Frekans Senkwonizasyon + Senkwonizasyon Kontni Otomatik + Presize peryòd repetisyon senkwonizasyon kontni an. Sistèm nan ap telechaje kontni ki seleksyone a selon frekans ou mansyone la a. + Senkwonize Kontni sou WIFI Sèlman + SI reglaj la aktive, senkwonizasyon kontni an ap fèt sèlman si aparèy la konekte sou yon rezo wifi, sinon, l ap ranvwaye pou lè gen yon koneksyon sou wifi ki disponib. + Senkwonizasyon + Chak jou + Chak semenn + Frekans Senkwonizasyon + Dezaktive Senkwonizasyon Kontni sou Wifi Sèlman? + SI reglaj la aktive, senkwonizasyon kontni an ap fèt sèlman si aparèy la konekte sou yon rezo wifi, sinon, l ap ranvwaye pou lè gen yon koneksyon sou wifi ki disponib. + Dezaktive + Manyèl + Mòd San Koneksyon + Pa Disponis San Koneksyon + Kontni sa a pa disponib sou mòd san koneksyon. + Kontni sa a pa disponib sou mòd san koneksyon. Si w vle chanje paramèt ou yo, ouvri ekran Kontni San Koneksyon an nan tablodbò a lè rezo a disponib. + San koneksyon + Senkwonizasyon Echwe + Telechajman %1$s sou %2$s + Annatant + Senkwonizasyon Kontni san Koneksyon an Fini + Senkwonizasyon Kontni san Koneksyon an Echwe + Anile Senkwonizasyon? + L ap kanpe senkwonizasyn kontni san koneksyon W ap fè l ankò pita. + Gen yonn oswa plizyè fichye ki pa rive senkwonize. Verifye koneksyon entènèt ou a epi eseye re voye yo ankò + Telechajman kòmanse + Kou yo paka ajoute nan favori yo san koneksyon. + Tout Kou + Kou + Gwoup + Tout Kou + Seleksyon Kou pou Tablodbò a ka fèt anliy sèlman. Ou ka navige nan detay kou san koneksyon yo. + Nòt + Reyisi! Telechaje %1$s sou %2$s + Senkwonizasyon Kontni San Koneksyon + Rejte notifikasyon + + %d kou an senkwonizasyon + %d kou yo ap senkwonize. + + Imaj kontni kou + Travay sa pa diponib ankò. + Ou pa konekte + Kounye a ou pa gen okenn kou ki disponib san koneksyon. + Senkwonizasyon Kontni san Koneksyon an Reyisi + Senkwonizasyon Kontni san Koneksyon an Echwe + Mizajou Senkwonizasyon san Koneksyon + Notifikasyon Canvas pou mizajou senkwonizasyon san koneksyon. + + %d kou a enkwonize. + %d kou yo senkwonize. + diff --git a/libs/pandares/src/main/res/values-id/strings.xml b/libs/pandares/src/main/res/values-id/strings.xml new file mode 100644 index 0000000000..bbaf905458 --- /dev/null +++ b/libs/pandares/src/main/res/values-id/strings.xml @@ -0,0 +1,1328 @@ + + + + File Tidak Ditemukan + Cari file + Masukkan istilah pencarian dengan tiga karakter atau lebih + + "Item yang cocok tidak ditemukan" + "Menemukan %d item yang cocok" + "Menemukan %d item yang cocok" + + Panda terbang mengenakan jubah + Kesulitan login? + Minta Bantuan Login + Saya kesulitan login + Silakan alamat email yang valid + Bagian: %s + Memilih: %s + + + + Detail Tugas + Jenis Penyerahan + Tidak Ada Konten + Batas + Berhasil diserahkan! + Penyerahan Anda sekarang menunggu dinilai + Mengunggah Penyerahan... + Ketuk untuk melihat kemajuan + Penyerahan Gagal + Ketuk untuk melihat detail + Penyerahan & Rubrik + Komentar & Rubrik + Ada masalah saat memuat tugas ini. Silakan periksa sambungan internet Anda dan coba lagi. + Diserahkan + Belum Diserahkan + Jenis File Yang Diizinkan + Upaya + Upaya Diizinkan + Upaya Digunakan + Tidak ada upaya tersisa + Serahkan Ulang Tugas + Luncurkan Alat Eksternal... + Luncurkan Alat Eksternal + Pratinjau + Satu atau lebih file gagal diunggah. Periksa sambungan internet Anda dan coba serahkan lagi. + %1$s dari %2$s + Penyerahan Sukses! + Tugas Anda berhasil diserahkan. Semoga harimu menyenangkan! + Batalkan Penyerahan + Ini akan membatalkan dan menghapus penyerahan Anda. + Penyerahan Dihapus + Tidak Ada + Dinilai + + + Pratinjau tidak tersedia untuk URL yang menggunakan \'http://\' + Masukkan URL yang valid. + Masukkan URL di sini untuk penyerahan Anda + Tampilkan Opsi Tambah File + Rekam Audio + Rekam Video + Editor Penyerahan Teks + + + Tulis… + Ada sesuatu yang salah saat mengunggah penyerahan. Serahkan lagi. + + + Penyerahan + Versi penyerahan + File (%d) + Rubrik + Kesalahan Penyerahan + Penyerahan ini adalah URL ke halaman eksternal. Kami telah menyertakan cuplikan tampilan halaman saat dikirim. + Belum Ada Penyerahan + Tugas Anda dikunci di %1$s pada %2$s. + Tugas Anda akan terbuka di %1$s pada %2$s. + Tugas Anda dikunci oleh modul \"%1$s\" + Tugas Anda dikunci oleh persyaratan modul + Tugas Terkunci + Penyerahan Tidak Diizinkan + Pratinjau URL yang dimasukkan + URL Situs Web + Tugas ini tidak mengizinkan penyerahan online + Tugas ini tidak mengizinkan penyerahan online + Tidak Ada Penyerahan Online + Penyerahan ini berkaitan dengan alat eksternal untuk penyerahan. + Alat Terbuka + Teks penyerahan + + + Dari %s poin + Dari %s pts + Dibolehkan + Nilai Final: %s + %1$s/%2$s + %1$s dari %2$s poin + Masukkan skor bagaimana-kalau + Rendah: %s + Rata-Rata: %s + Tinggi: %s + + + Skor khusus + Tidak ada rubrik untuk penugasan ini + Lihat deskripsi panjang + + + Mengunggah file %1$d dari %2$d + Mengunggah komentar untuk %s + Mengunggah file media + Unggahan komentar gagal untuk %s + Lampirkan file ke komentar Anda dengan mengetuk satu opsi di bawah ini + Ada pertanyaan tentang tugas Anda?\nKirim pesan kepada instruktur Anda. + Pesan ini tidak dapat dikirim. Ketuk untuk mencoba lagi. + Unggahan Media + Unggah Media - Audio + Unggah Media - Video + Penyerahan Teks + Penyerahan Alat Eksternal + Penyerahan Diskusi + Penyerahan Kuis + Upaya %d + File Media + Audio + Video + + + Versi: + + + Logo Canvas + Masukkan URL Canvas Anda: + Masukkan nilai + myschool.instructure.com + Coba masuk ke Canvas Network? + Aplikasi ini tidak diizinkan untuk digunakan + Server yang Anda masukkan tidak diizinkan untuk aplikasi ini. + Agen pengguna untuk aplikasi ini tidak diizinkan. + Kami tidak dapat memverifikasi server untuk digunakan bersama aplikasi ini. + Ok + Opsi + + Lampirkan file + Edit + + + Hapus pengguna ini? + Anda harus masuk kembali ke pengguna ini lagi untuk mengakses kontennya. + + Bookmark + Tambah Bookmark + Label + Label Harus Ada + Bookmark Dibuat + Bookmark tidak dapat dibuat + Pilih Bookmark + Bookmark Dihapus + Hapus Bookmark? + Edit Bookmark + Bookmark Diperbarui + Buat bookmark lalu lihat di sini! + + + Hari Ini + Masa Lalu + Tidak Ada Tanggal + Mendatang + 7 Hari Berikutnya + Jatuh tempo hari ini pada %s + Jatuh tempo besok pada %s + Jatuh tempo kemarin pada %s + Tugas Anda tidak memiliki tanggal batas + Batas waktu %1$s pada %2$s + am + pm + + + + Terkunci + Tugas + Tidak ada deskripsi untuk tugas ini + Tidak ada Tugas dalam grup ini + Tugas ini dikesampingkan dan tidak akan dipertimbangkan dalam penghitungan total + EX + Sortir menurut + Waktu + Ketikkan + Sortir menurut Waktu + Sortir menurut Jenis + Batal + Semua + Sortir tombol tugas, sortir menurut waktu + Sortir tombol tugas, sortir menurut waktu + Tugas disortir menurut waktu + Tugas disortir menurut jenis + + + Poin\u0020 + Simpan + Batal + + + Balasan hanya tampak oleh mereka yang telah memposting setidaknya satu balasan. + File ini saat ini dikunci + Editor Balasan Diskusi + + + Memulai + Mengakhiri + Acara Satu Hari Penuh + di + Kalender Pribadi + Lokasi Tidak Ditetapkan + + Diskusi + Ditutup untuk Komentar + Unggahan Diskusi + Total: + T/A + Berdasarkan tugas yang dinilai + Tampilkan Skor Bagaimana-Kalau + Skor Bagaimana-Kalau + %1$s/%2$s (%3$s) + %1$s dari %2$s poin, %3$s + Kotak Masuk + Belum dibaca + Diarsipkan + Terkirim + Kelas + Bersihkan filter + Kirim Pesan + Monolog + Tidak ada item untuk ditampilkan + Harus Dilakukan + Filter Kursus + Filter menurut... + Kursus Difavoritkan + Acara + Acara berhasil dihapus + Ikon Waktu + Serahkan + Tidak Ada Batas Waktu + + Batas waktu pada + Sampai batas waktu pada %s + Batas + Batas %1$s + + %d membutuhkan penilaian + %d membutuhkan penilaian + + Buat Folder + Buat Folder + Buat File + Tampilkan Tombol Buat File dan Buat Folder + Sembunyikan Tombol Buat File dan Buat Folder + Terjadi kesalahan selama pembuatan folder. + Ubah Nama + Ubah nama file + Ubah nama folder + Nama tidak boleh kosong + Anda yakin mau menghapus file \'%s\'? Tindakan ini tidak bisa diurungkan + Anda yakin mau menghapus folder \'%s\'? Tindakan ini tidak bisa diurungkan + + Anda yakin ingin menghapus folder \'%1$s\', termasuk %2$d item di dalamnya? Tindakan ini tidak bisa diurungkan + Anda yakin ingin menghapus folder \'%1$s\', termasuk %2$d item di dalamnya? Tindakan ini tidak bisa diurungkan + + + Pengaturan + Nama + Email + Nama pengguna + Kata sandi + Autentikasi Dibutuhkan + Email atau kata sandi tidak valid + ID Login + Domain + Kirim Umpan Balik + Kirim Email… + Edit + + %d item + %d item + + Tambah ke Beranda + Arsipkan + Pergi ke Kotak Masuk + Tandai Belum Dibaca + Pilih Orang + Hapus + Hapus Acara + Memilih + Terjadi kesalahan saat mencoba mengirim pesan Anda. Silakan coba lagi. + Percakapan itu telah dihapus. + Gambar avatar baru gagal diunggah + Edit Foto + Nama pengguna tidak valid. + Nama pengguna berhasil diperbarui! + Ambil foto + Pilih foto dari galeri + File tidak ditemukan. + + + Balas + Tulis pesan... + Tulis + Buat Pesan + Pesan berhasil dikirim. + Pesan tidak boleh kosong! + Tidak Ada Penerima + Kirim + Pengguna + Individu + Versi: + v. %s + Semua Kursus + Semua Grup + Opsi Kursus + Opsi kursus untuk %s + Edit nama panggilan + Edit warna kursus + Buka + Buka dengan aplikasi alternatif + Membuka File… + Unduh + Lampiran Pesan + Lampiran + Ikon Lampiran + OK + dengan + Mulai Percakapan + Hapus Percakapan + Dibagikan dengan \u0020 + Dibagikan kepada Anda + Ketuk \"+\" untuk membuat percakapan baru. + Semua + Filter Kotak Masuk + Pilih kursus atau grup + Tidak ada pesan + Hapus lampiran + Unduh Lampiran + Opsi Pesan + Maju + Balas Semua + Keluarkan dari arsip + Anda yakin mau menghapus salinan Anda dari pesan ini? Tindakan ini tidak bisa diurungkan + Anda yakin mau menghapus salinan Anda dari percakapan ini? Tindakan ini tidak bisa diurungkan + Tidak dapat melakukan tindakan ini. Silakan periksa sambungan internet Anda dan coba lagi. + Percakapan diarsipkan + Percakapan dikeluarkan dari arsip + Pesan dihapus + Kirim pesan individu ke setiap penerima + Anda tidak diizinkan mengirim pesan kepada satu atau lebih dari penerima yang dipilih. + Pesan Baru + Teruskan Pesan + Tambah penerima lain. Pesan yang dialamatkan hanya untuk diri sendiri tidak dapat dikirim. + Pilih Penerima + + + %d orang + %d orang + + + + %d grup + %d grup + + + Keluar tanpa menyimpan? + Anda yakin mau keluar tanpa menyimpan? + Keluar + + + Pesan Grup? + Tambah semua orang ke dalam percakapan grup tunggal, atau kirim pesan ke semua orang secara individu? + Grup + Grup + Anggota Grup + + Memuat… + Pilih dari daftar + + Silabus + Rangkuman + Orang + Guru & TA + Siswa + Pengamat + Silabus belum ditambahkan. + Terjadi kesalahan saat memuat modul Anda. + + Canvas + Pilih Penerima + Pesan ini saat ini tidak memiliki penerima. + Kirim Pesan + Avatar Pengguna + + Ikon Tugas + Ikon Pengumuman + Ikon Percakapan + Ikon Default + Ikon Diskusi + Ikon Nilai + Nilai + Semua Periode Penilaian + Kalender + Bookmark + Tampilkan Nilai + Overlay Warna + Nilai tidak tampak untuk kursus ini. + + Seluruh grup ini sudah dipilih. + Tidak ada pengguna di dalam grup ini. + + + Dihapus + + Memuat Konten Canvas… + + UnknownDevice + + Tautan yang dipilih adalah untuk domain selain yang diberikan kepada Anda. + + Halaman + Informasi halaman tidak tersedia. + Tidak ada halaman yang tersedia ini + Dimodifikasi Terakhir: + Dimodifikasi Terakhir: %1$s + + Mulai Bertindak sebagai Pengguna + Stop Bertindak sebagai Pengguna + ID Pengguna + Terjadi kesalahan saat mencoba bertindak sebagai pengguna + + ID tidak boleh kosong + + Pergi Ke Kuis + + Kuis + + Posting terakhir + + Pengumuman Baru + Diskusi Baru + Buat Diskusi + Buat Pengumuman + + Kirim Pengumuman + Kirim Diskusi + + Pengumuman berhasil diposting. + Diskusi berhasil diposting. + Diskusi berhasil diperbarui. + Rancangan diskusi berhasil dibuat. + + Terjadi kesalahan saat memposting pengumuman. + Terjadi kesalahan saat memposting diskusi. + Izinkan Balasan Berutas + Pengguna harus posting sebelum melihat balasan + Izinkan pengguna berkomentar + Pesan + Judul + + + Pesan tidak boleh kosong. + + Maaf. Anda tidak memiliki izin untuk mengirim posting pengumuman dalam kursus ini. + Maaf. Anda tidak memiliki izin untuk mengirim posting diskusi dalam kursus ini. + + Pengumuman + + Judul tidak boleh kosong. + + + Modul + Lihat item ini + Anda telah melihat item ini + Harus menyerahkan tugas + Tugas diserahkan + Berkontribusi ke halaman ini + Anda telah berkontribusi + Mendapat skor setidaknya + Skor minimum terpenuhi + Prasyarat: + Terbuka: + Terkunci + Tugas ini adalah bagian dari modul %s dan belum dibuka. + Halaman ini adalah bagian dari modul %s dan belum dibuka. + File ini adalah bagian dari modul %s dan belum dibuka. + Kuis ini adalah bagian dari modul %s dan belum dibuka. + Diskusi ini adalah bagian dari modul %s dan belum dibuka. + Anda pertama-tama harus menyelesaikan: + Ini akan terbuka pada: + Pergi Ke Modul + Tandai selesai + Item Modul Tidak Ditemukan + Terjadi kesalahan saat memuat modul Anda. + + Bantu + Pertanyaan Instruktur + Tautan + + + Tugas ini terkunci. + + + Kursus Saya + + + + Entri Teks + URL Online + Penyerahan ini hanya menerima satu unggahan file + Tambah Entri Situs Web + Rekaman Media + Serahkan + + + + Kemajuan Belum Disimpan + Informasi yang belum disimpan akan hilang. Anda ingin melanjutkan? + / + + + Konfirmasi + + + Tandai sebagai selesai + + + Berikutnya + Tambah komentar… + + Beranda + Notifikasi + + Ikon + Folder Pengguna Root + Folder Kursus Root + Folder Grup Root + + + Tersedia secara privat + Tersedia secara publik + Memulai: \u0020 + Kode Kursus: \u0020 + Berakhir: \u0020 + Visibilitas: \u0020 + Lisensi: \u0020 + + + + Kolaborasi + Konferensi + Obrolan + Capaian + + + Kunjungi Halaman: + Cuplikan situs web diambil saat Anda menyerahkannya. Ketuk dan tahan gambar di bawah ini untuk membuka atau mengunduh gambar penuh. + Penyerahan ini adalah URL ke halaman eksternal. Ingatlah bahwa halaman ini mungkin telah berubah sejak penyerahan pertama kali dilakukan. + Pratinjau dari url yang diserahkan + + + %1$s tidak didukung. + Tautan tidak didukung. + Buka di Browser + Tidak didukung + + + + Profil + + (Tanpa Subjek) + Subjek + Gambar Panda Sedih + Fakta Panda:  + Hapus + Pendiri + + EULA + Kebijakan Privasi + Ketentuan Penggunaan + Canvas di GitHub + + + Kuis Tugas + Kuis Latihan + Survei Dinilai + Survei + + + + Harus Dilakukan + Nilai + Notifikasi + Canvas - To Do + Canvas - Nilai + Canvas - Notifikasi + Anda tidak login + Pilih gaya widget Anda + Sembunyikan detail di widget + Terang + Gelap + + + Bertanya kepada Instruktur Anda + Pertanyaan diserahkan kepada instruktur Anda + Cari di Panduan Canvas + Panduan Canvas + Temukan jawaban untuk pertanyaan umum + Laporkan masalah + Jika aplikasi bermasalah, beri tahu kami + Minta Fitur + Punya ide untuk meningkatkan app? + Bagikan Cinta Anda untuk Aplikasi + Beri tahu kami bagian favorit Anda dari aplikasi + + + Ide untuk Canvas [Android] + Informasi berikut akan membantu kami memahami ide Anda lebih baik: + + + Pertanyaan ini tentang kursus yang mana? + Pesan akan dikirim ke semua Guru dan TA di kursus. + Sedang mengirim… + Kesalahan + + + Kursus + Daftar To Do + Notifikasi + Notifikasi Push + Notifikasi Push belum didaftarkan untuk perangkat ini. + Pengaturan Profil + Preferensi Akun + PIN dan Sidik Jari + Pasangkan dengan Pengamat + Minta orang tua Anda memindai kode QR ini dari app Canvas Parent untuk pairing dengan Anda. Kode ini akan kedaluwarsa dalam tujuh hari, atau setelah satu kali penggunaan. + Kode Pairing: \u0020 + Kesalahan Kode Pairing + Tidak dapat mengambil kode pairing. Fitur ini hanya didukung untuk siswa. + + Buka SpeedGrader + + todo to do todos todo daftar + kursus kursus kelas kelas + nilai nilai + + + Unggah Ke + Unggah Ke Canvas + File Saya + File Kursus + Unggah ke Komentar Penyerahan + + Changelog + + Edit Nama Pengguna + + Ambil foto baru + Pilih dari galeri + Atur ke default + Pilih gambar latar + + Ketuk untuk menambah ke kursus + Tidak ada kursus di sini, silakan istirahat! + Silakan mendaftar di kursus untuk melihat nilai Anda + + Buka laci navigasi + Tutup laci navigasi + + Halaman Awal + Tidak dapat menemukan pendaftaran kursus. + Tidak dapat menemukan pendaftaran grup. + Terjadi kesalahan yang tidak terduga. + Halaman ini tersembunyi atau terkunci dan tidak dapat diakses. + + Tutup + Ditutup + + Tugas Jatuh Tempo + Tugas Mendatang + Tugas Tidak Bertanggal + Tugas Yang Lalu + + Terjadi kesalahan saat mengambil kursus untuk item ini. + Terjadi kesalahan saat mengambil grup untuk item ini. + + Pilih latar + + Menyerahkan file… Periksa bilah notifikasi untuk info terbaru. + Selesai menyerahkan file + + Bio + Rancangan + Terbitkan + + Buat Avatar Panda + Kembali + Atur sebagai avatar + Avatar panda berhasil disimpan + Avatar berhasil disimpan. + Terjadi kesalahan saat menyimpan avatar panda + Avatar Panda + Kepala Avatar Panda + Badan Avatar Panda + Kaki Avatar Panda + PandaAvatars + Bagikan + + SpeedGrader + Gauge + Slider nilai + + Buat Acara Baru + Non-aktifkan panda + + Pengumuman + Tambah Akun + + Ubah Pengguna + + Silakan periksa sambungan data Anda dan coba lagi. + + + Notifikasi Canvas + Notifikasi Canvas Umum + Konfigurasi notifikasi lebih lanjut dapat dilakukan dari dalam bagian Preferensi Notifikasi Canvas. + + Aktivitas Kursus + Diskusi + Percakapan + Menjadwalkan + Grup + Peringatan + Konferensi + + Batas Waktu + Kebijakan Penilaian + Konten Kursus + File + Pengumuman + Pengumuman yang Anda Buat + Penilaian + Undangan + Semua Penyerahan + Penilaian Terlambat + Komentar Penyerahan + + Diskusi + Posting Diskusi + + Tambah Ke Percakapan + Pesan Percakapan + Percakapan Yang Anda Buat + + Pendaftaran Janji Temu Siswa + Pendaftaran Janji Temu + Pembatalan Janji Temu + Ketersediaan Janji Temu + Kalender + + Pembaruan Keanggotaan + Notifikasi Administratif + Rekaman Siap + + Email + Perangkat + SMS + + Notifikasi Perangkat + Apakah Anda ingin mematikan notifikasi perangkat? Pengaturan ini dapat diubah nanti di Pengaturan > Notifikasi > Untuk Semua Perangkat + Notifikasi telah diaktifkan. + + Dapatkan notifikasi saat tanggal batas tugas berubah. + Dapatkan notifikasi saat kebijakan penilaian kursus berubah. + Dapatkan notifikasi saat konten di Wikipage, Kuis, dan Tugas berubah. + Dapatkan notifikasi saat file baru ditambahkan ke kursus Anda. + Dapatkan notifikasi saat ada pengumuman baru di kursus Anda. + Dapatkan notifikasi saat Anda membuat pengumuman dan saat seseorang membalas pengumuman Anda. + Dapatkan notifikasi saat tugas/penyerahan dinilai/berubah dan saat bobot nilai berubah. + Dapatkan notifikasi untuk undangan ke konferensi web, grup, kolaborasi, tinjauan sejawat, dan pengingat. + Khusus Instruktur dan Admin. Dapatkan notifikasi saat tugas diserahkan atau diserahkan ulang. + Khusus Instruktur dan Admin. Dapatkan notifikasi saat tugas terlambat diserahkan. + Dapatkan notifikasi saat komentar diberikan pada penyerahan Anda. + Dapatkan notifikasi saat ada topik diskusi di kursus Anda. + Dapatkan notifikasi saat ada posting baru di diskusi yang Anda langganani. + Dapatkan notifikasi saat Anda ditambahkan ke percakapan. + Dapatkan notifikasi saat Anda memiliki pesan baru di kotak masuk. + Dapatkan notifikasi saat Anda membuat percakapan baru. + Khusus Instruktur dan Admin. Dapatkan notifikasi saat ada pendaftaran janji temu. + Dapatkan notifikasi saat ada pendaftaran baru di kalender Anda. + Dapatkan notifikasi saat ada pembatalan janji temu. + Dapatkan notifikasi saat slot janji temu tersedia. + Dapatkan notifikasi tentang item kalender baru dan diperbarui. + Khusus admin, menunggu pendaftaran diaktifkan. Dapatkan notifikasi saat pendaftaran grup diterima atau ditolak. + Khusus Instruktur dan Admin. Dapatkan notifikasi tentang pendaftaran kursus, laporan yang dibuat, konten yang diekspor, laporan migrasi, pengguna akun baru, dan grup siswa baru. + Dapatkan notifikasi saat rekaman konferensi siap. + + Tanpa batas + + Pertanyaan + + %d pertanyaan + %d pertanyaan + + + + %s poin + %s poin + + + Batas Waktu + %1$s Notifikasi Baru + suka + Sukai Entri + suka + + %s suka + %s suka + + + Merah + Hot Pink + Lavender + Violet + Ungu + Slate + Biru + Sian + Hijau + Chartreuse + Kuning + Emas + Oranye + Merah Muda + Abu-Abu + + Edit Nama Panggilan Kursus + Nama kecil kursus tidak dapat diatur pada saat ini. + + Warna Kursus + Personalisasikan kursus Anda dengan mengatur warna baru. + Warna kursus tidak dapat diatur pada saat ini. + + Merah Muda + Hot Pink + Violet + Ungu + Biru Gelap + Biru + Sian + Aqua Blue + Emerald Green + Hijau + Chartreuse + Kuning + Oranye + Dark Orange + Merah + + Kursus %s, favorit. + Kursus %s, bukan favorit. + Grup %s, favorit. + Grup %s, bukan favorit. + Grup Akun + + + Dari layar kustomisasi, tambah kursus atau grup untuk melihatnya di sini. + + Bilah alat tertutup + Tampilkan di \"Kursus Saya\" + Tentang + Tambah pintasan ke kursus Anda + Nama Kecil Kursus + + Fungsionalitas tidak tersedia saat offline + Edit Dashboard + Pilih kursus yang Anda ingin lihat di Dashboard + Edit daftar kursus Anda + + Dasboard + Sebelumnya + Hal. %d dari %d + Bahasa + Default Sistem + Memulai Ulang Canvas + Mengubah bahasa mengharuskan aplikasi dimulai ulang, Anda yakin? + Bahasa default sistem Anda tidak dijamin didukung dan membutuhkan mulai ulang, Anda yakin? + Terkunci hingga \"%s\" dinilai + Ikon Terkunci + Pilih Grup tugas + Pilih Jalur tugas + Pilih + Pilihan %d + Memperbarui informasi modul… + Menunggu tinjauan + %s poin + Poin Total + %s poin + Skor + %s / %s poin + Lihat Semua + Selamat datang! + Singkirkan + Ketuk untuk melihat pengumuman + Terima + Tolak + dari + + + Tidak ada file yang terkait dengan kursus ini. + Tidak ada file yang terkait dengan grup ini. + Jenis File yang Tidak Didukung + Tugas ini hanya mengizinkan jenis file tertentu: %s + + + B + KB + MB + GB + TB + + + Jalankan tautan di browser eksternal + Alat LTI ini tidak dapat dimuat saat ini. + Penyerahan URL + + Aktivitas Terkini + Modul + Tugas + Silabus + + Muat Ulang Widget + + Salin + Anda telah diundang. + Undangan diterima! + Undangan ditolak. + Penalti terlambat (%s) + Filter Nilai + + 99+ + + Buka di webview + Tidak didukung di perangkat ini + + Mengunduh + Pengunduhan gagal + Unduhan berhasil + + Detail tanggal batas lengkap + Penyerahan + Lampiran + Kembali + + + %s poin + %s poin + + + %s poin + %s poin + + Kepada + + Kami tidak dapat menemukan aplikasi eksternal untuk melihat alat LTI ini. + + Unggahan Komentar + + Halaman Depan + Edit Halaman + Deskripsi + Halaman berhasil diperbarui. + Kesalahan terjadi saat mencoba menyimpan halaman ini. Cobalah lagi. + Judul halaman harus ditetapkan. + Detail Halaman + Sedang menyimpan + Anda yakin mau menghapus acara ini? + Konferensi belum didukung di seluler. + Gambar pratinjau file + Kesalahan terjadi saat mencoba memuat PDF ini. + Maaf! Fitur ini tidak diizinkan untuk tampilan siswa. + Tidak Ada yang Bisa Dilihat di Sini. + Fitur Tidak Didukung + + + Tambah Siswa + Masukkan kode pairing siswa yang diberikan kepada Anda. + Kode Pairing... + Pairing Gagal. Pastikan kode pairing Anda benar dan dalam batas waktu penggunaan. + Lengkap + Tidak lengkap + + + Pengaturan Posting + Posting Nilai + Sembunyikan Nilai + + %d nilai saat ini diposting + %d nilai saat ini diposting + + + %d nilai saat ini disembunyikan + %d nilai saat ini disembunyikan + + Posting ke... + Bagian Spesifik + Semua Orang + Nilai akan dibuat tampak untuk semua siswa + Dinilai + Nilai akan dibuat tampak untuk siswa dengan penyerahan yang telah dinilai + Semua Tersembunyi + Semua nilai saat ini tersembunyi. + Semua Diposting + Semua nilai saat ini diposting. + Nilai Diposting + Nilai Disembunyikan + Gagal memposting nilai + Gagal menyembunyikan nilai + Nilai sebelum posting + Nilai setelah posting + Override nilai + Nilai Saat Ini + Membuka di Canvas Student + Tampilan Siswa + Beri Nilai di Play Store + + + Kepala dipilih: %s + Badan dipilih: %s + Kaki dipilih: %s + Baju hati ungu dengan pinggiran merah muda + Baju bintang hijau dengan pinggiran biru + Baju bintang merah dengan pinggiran oranye + Blazer biru, dasi kupu-kupu merah, dan celana panjang abu-abu + Baju oranye dan jins + Baju merah dan celana panjang cokelat + T-shirt grafik matahari kuning dan celana panjang abu-abu + T-shirt grafik basket teal dan celana panjang ungu + T-shirt grafik android biru dan celana panjang hijau + T-shirt grafik instruktur putih dan celana panjang biru + T-shirt bintang tiga abu-abu tua dan celana panjang teal + Seragam penyihir burgundy dengan tongkat sihir dan celana panjang hitam + Torsi panda telanjang + Kacamata aviator hitam teardrop dan lipstick + Dandanan dan pita rambut ungu + Kumis gaya + Kacamata pesta hijau + Kacamata cokelat + Topeng penyamaran emas dan lipstick + Kacamata aviator hitam teardrop + Pipi bebercak + Penyihir kening codet berkaca mata + Kaki telanjang + Sepatu merah muda dengan pita ungu + Sepatu biru dengan pita hijau + Sepatu merah dengan pita oranye + Sepatu merah + Pilih kepala + Pilih badan + Pilih kaki + + + 00:00:00 + %1$d jam, %2$d menit, dan %3$d detik + %1$s dari %2$s + Putar Ulang + Berhenti + Mulai rekaman audio + Stop rekaman audio + Mulai rekaman video + Stop rekaman video + Tutup tampilan rekaman + Hapus rekaman + Putar Ulang Komentar Video + Terjadi kesalahan saat mencoba melihat pemutaran ulang. + Terjadi kesalahan saat mencoba menyerahkan komentar Anda. + + Tambah komentar video + Tambah komentar audio + Kesalahan tidak terduga terjadi saat mencoba merekam audio. + Kesalahan tidak terduga terjadi saat mencoba merekam video. + + Pertanyaan: + Batas Waktu: + Upaya Diizinkan: + Petunjuk + Tidak ada + Lihat Kuis + Lihat Diskusi + Tugas ini dikunci oleh modul \"%1$s\". + Pilih File Media + Penulis Tidak Dikenal + Tanggal Tidak Diketahui + %s. minus + %s. + %s %s + %s %s, %s + + + %s Menit + %s Menit + + + + Tidak Ada Konferensi + Belum ada konferensi untuk ditampilkan. + Tidak ada deskripsi untuk konferensi ini + Terjadi kesalahan saat memuat konferensi Anda + Selesai %s pada %s + Mulai %s pada %s + Tidak Dimulai + Sedang Berlangsung + Gabung + Konferensi Baru + Merampungkan Konferensi + Detail Konferensi + Rekaman + Konferensi sedang berlangsung + + Peringkat kriteria %s + %s, %s + informasi selengkapnya + Tombol Buat File dan Folder tampak + Tombol Buat File dan Folder tidak tampak + Hapus Pilihan Semua + Pilih Semua + Semua kursus + Semua grup + Pilih kursus untuk Dashboard atau navigasikan ke detail kursus. + Pilih grup untuk Dashboard atau navigasikan ke detail kursus. + Pendaftaran saat ini + Pendaftaran terdahulu + Pendaftaran mendatang + Ditambah ke Dashboard + Dihapus dari Dashboard + Semua ditambahkan ke Dashboard + Semua dihapus dari Dashboard + Kursus tidak aktif tidak dapat ditambahkan ke Dashboard. + Edit Dashboard + Tidak ada kursus di sini + Anda harus terdaftar dalam kursus untuk menambahkannya ke Dashboard. + Hapus dari Dashboard + Hapus semua dari dashboard + Tambah ke dashboard + Tambah semua ke dashboard + + + Akun + Homeroom + Jadwalkan + Nilai + Sumber Daya + Selamat datang %1$s! + Subjek Saya + Lihat Pengumuman Sebelumnya + Gagal memuat Homeroom + Gagal memuat ulang Homeroom + Tidak Batas Waktu Hari Ini + %1$s harus diserahkan hari ini + %1$s tidak ada + Selamat datang! + Subjek Anda ditampilkan di sini. + Anda saat ini tidak memiliki subjek. + Membutuhkan mulai ulang app + Pilih + Pilih Periode Penilaian + Periode Penilaian Saat Ini + Gagal memuat nilai + Gagal memuat ulang nilai untuk periode penilaian + Gagal memuat ulang nilai + Tidak ada nilai untuk ditampilkan + Tidak Dinilai + Ubah periode penilaian + Nilai tidak tersedia + Homeroom + Tampilan Homeroom + Tautan Penting + Aplikasi Siswa + Info Kontak Staf + Pilih Kursus + Tautan Penting + Guru + Asisten Guru + Sumber daya Anda ditampilkan di sini. + Gagal memuat sumber daya + Gagal memuat ulang sumber daya + + Buka tampilan alternatif yang lebih dapat diakses + + Penerima + Subjek + Pilih kursus, %s + + Respons siswa ini disembunyikan karena tugas ini anonim. + + Anda telah menandainya sebagai selesai. + Tidak dapat mengubah batas waktu saat masuk batas dalam periode penilaian tertutup + Lompat ke Hari Ini + %1$s, %2$s + kirim pesan + + Ada sesuatu yang salah + Gagal memuat kuis + Gagal memuat penyerahan + Anotasi Siswa + Anotasi Siswa + Tanpa batas waktu + Tanpa batas waktu + Tidak Ada + Jangan hapus + Simpan rancangan? + Perubahan Anda, kalau tidak, tidak akan disimpan + Ketuk di sini untuk melanjutkan + Rancangan Tersedia + Kesalahan terjadi saat memuat penyerahan + Ketuk konten penuh + Ketuk untuk membuka di aplikasi eksternal + Tanggal Penting + Tidak ada tanggal penting + Tanggal Penting + + Pustaka Komentar + Saran tidak tersedia + Komentar + %s ditandai sebagai selesai + Kesalahan terjadi, silakan coba lagi. + Terima undangan + Tolak undangan + Tidak ada notifikasi untuk ditampilkan + + Pemindaian Dokumen + Warna + Grayscale + Monokrom + Asli + Perangkat Anda tidak memiliki aplikasi apa pun yang terinstal untuk membuka tautan ini. + + Diskusi anonim saat ini tidak didukung di seluler. Buka di browser untuk melihat diskusi. + Buka di browser + + Alihkan ke tampilan daftar + Alihkan ke tampilan grid + + Pilih tema aplikasi + Tema Aplikasi + Terang + Gelap + Sama dengan perangkat + + Canvas sekarang tersedia dalam tema gelap + Pilih tema aplikasi + Tema terang + Tema gelap + Sama seperti tema perangkat + Simpan + Anda dapat mengubahnya nanti di pengaturan aplikasi + + Ambil + Unggahan File + + + %s pesan belum dibaca + %s pesan belum dibaca + + + + %s notifikasi belum dibaca + %s notifikasi belum dibaca + + + Anotasi tidak dipilih + + Notifikasi Email + Segera + Setiap Hari + Mingguan + Tidak Pernah + Pilih frekuensi + Tutup dialog kemajuan + %1$s dari %2$s + Mengunggah ke File + Mengunggah penyerahan ke \"%s\" + + Mengunggah Penyerahan + Mengunggah File + Batalkan Penyerahan + Ini akan membatalkan dan menghapus penyerahan Anda. + Unggahan File Gagal + Unggah ke Canvas untuk %s. + Unggah ke File Saya + Serahkan tugas + Pilih kursus + Pilih tugas + %1$s, %2$s + Batalkan Unggahan? + Ini akan membatalkan unggahan Anda. + Filter tugas + Filter Tugas + Batal + Semua + Terlambat + Tidak Ada + Dinilai + Segera Hadir + Dibuat oleh Student View + Kesalahan terjadi. Topik mungkin tidak lagi tersedia. + Izin kamera ditolak secara permanen. Buka pengaturan app untuk mengizinkannya. + diff --git a/libs/pandares/src/main/res/values-is/strings.xml b/libs/pandares/src/main/res/values-is/strings.xml index 71498f4b73..05e63a199b 100644 --- a/libs/pandares/src/main/res/values-is/strings.xml +++ b/libs/pandares/src/main/res/values-is/strings.xml @@ -1391,4 +1391,77 @@ Tölvupóstur Útgáfa Upp kom vandamál við að endurhlaða þetta verkefni. Athugaðu tengingu þína og reyndu aftur. + Merki Instructure + Kjörstillingar + Efni án nettengingar + Samstilling + + + Efni án nettengingar + Stjórna efni án nettengingar + Geymsla + %s af %s notað + Önnur smáforrit + Canvas nemandi + Eftir + Öll námskeið + Samhæfa + %d Valið + Velja allt + Afvelja allt + Villa kom fram við að hlaða innihaldið. + Með því að virkja sjálfvirka samstillingu efnis mun hún sjá um að hlaða niður völdu efni byggt á stillingunum hér að neðan. Samstilling efnis mun gerast jafnvel þótt forritið sé ekki í gangi. Ef slökkt er á stillingunni mun engin samstilling eiga sér stað. Efni sem þegar hefur verið hlaðið niður verður ekki eytt. + Samstillingartíðni + Sjálfvirk samstilling efnis + Tilgreindu endurtekningu samstillingar efnis. Kerfið mun hlaða niður völdu efni miðað við tíðnina sem tilgreind er hér. + Samstilltu efni aðeins yfir Wi-Fi + Ef þessi stilling er virkjuð mun samstilling efnis aðeins eiga sér stað ef tækið tengist Wi-Fi neti, annars verður henni frestað þar til Wi-Fi net er tiltækt. + Samstilling + Daglega + Vikulega + Samstillingartíðni + Slökkva á samstillingu efnis aðeins yfir Wi-Fi? + Ef þessi stilling er virkjuð mun samstilling efnis aðeins eiga sér stað ef tækið tengist Wi-Fi neti, annars verður henni frestað þar til Wi-Fi net er tiltækt. + Slökkva + Handvirkt + Án nettengingar hamur + Ekki fáanlegt án nettengingar + Þetta efni er ekki fáanlegt í án nettengingar ham. + Þetta efni er ekki fáanlegt í án nettengingar ham. Ef þú vilt breyta stillingum þínum opnaðu skjáinn Efni án nettengingar frá mælaborðinu þegar net er í boði. + Án nettengingar + Samstilling mistókst + Sæki %1$s af %2$s + Í biðröð + Samstillingu efnis án nettengingar lokið + Samstilling efnis án nettengingar mistókst + Hætta við samhæfingu? + Það mun stöðva samstillingu efnis án nettengingar. Þú getur gert það aftur síðar. + Ekki tókst að samstilla eina eða fleiri skrár. Athugaðu nettenginguna þína og reyndu aftur að skila. + Niðurhal hefst + Ekki er hægt að bæta námskeiðum við eftirlæti án nettengingar. + Öll námskeið + Námskeið + Hópar + Öll námskeið + Að velja námskeið fyrir mælaborð er aðeins hægt að gera á netinu. Þú getur flett til námskeiðsupplýsingar án nettengingar. + Athugasemd + Tókst! Sótt %1$s af %2$s + Samstillir efni án nettengingar + Hafna tilkynningu + + %d námskeið er að samstillast. + %d námskeið eru að samstillast. + + Myndir af innihaldi námskeiðs + Þetta verkefni er ekki lengur tiltækt. + Þú ert án nettengingar + Þú ert ekki með nein námskeið sem eru í boði án nettengingar. + Samstilling efnis án nettengingar tókst + Samstilling efnis án nettengingar mistókst + Uppfærslur samstillingar án nettengingar + Canvas tilkynningar fyrir uppfærslur samstillingar án nettengingar. + + %d námskeið hefur verið samstillt. + %d námskeið hafa verið samstillt. + diff --git a/libs/pandares/src/main/res/values-it/strings.xml b/libs/pandares/src/main/res/values-it/strings.xml index 1ebe6ca67a..1ddabbeb86 100644 --- a/libs/pandares/src/main/res/values-it/strings.xml +++ b/libs/pandares/src/main/res/values-it/strings.xml @@ -1391,4 +1391,77 @@ E-mail Versione Si è verificato un problema durante la ricarica di questo compito. Verifica la connessione e riprova. + Logo Instructure + Preferenze + Contenuto offline + Sincronizzazione + + + Contenuto offline + Gestisci contenuto offline + Spazio di archiviazione + %s di %s usato + Altre app + Studente Canvas + Rimasto + Tutti i corsi + Sincronizza + %d selezionato/i + Seleziona tutto + Deseleziona tutto + Si è verificato un errore durante il caricamento dei contenuti. + L’attivazione della Sincronizzazione contenuto automatica si occuperà del download del contenuto selezionato in base alle impostazioni riportate di seguito. La sincronizzazione del contenuto verrà effettuata anche se l’applicazione non è in funzione. Se l’impostazione è disattiva, non sarà effettuata alcuna sincronizzazione. Il contenuto già scaricato non sarà eliminato. + Frequenza di sincronizzazione + Sincronizzazione contenuto automatica + Specificare la ripetizione della sincronizzazione del contenuto. Il sistema scaricherà il contenuto selezionato in base alla frequenza specificata qui. + Sincronizza contenuto solo con Wi-Fi + Se questa impostazione è attiva, la sincronizzazione del contenuto sarà eseguita solo se il dispositivo si collega ad una rete Wi-Fi, diversamente sarà posticipata fino a quando non sarà disponibile una rete Wi-Fi. + Sincronizzazione + Ogni giorno + Ogni settimana + Frequenza di sincronizzazione + Disattivare la sincronizzazione contenuti solo su Wi-Fi? + Se questa impostazione è attiva, la sincronizzazione del contenuto sarà eseguita solo se il dispositivo si collega ad una rete Wi-Fi, diversamente sarà posticipata fino a quando non sarà disponibile una rete Wi-Fi. + Disattiva + Manuale + Modalità offline + Non disponibile offline + Questo contenuto non è disponibile in modalità offline. + Questo contenuto non è disponibile in modalità offline. Se si desidera cambiare le impostazioni, aprire la schermata Contenuto offline dalla dashboard quando la rete è disponibile. + Offline + Sincronizzazione non andata a buon fine + Download di %1$s di %2$s + Aggiunto alla coda + Sincronizzazione contenuto offline completata + Sincronizzazione contenuto offline non andata a buon fine + Cancellare sincronizzazione? + Ferma la sincronizzazione del contenuto offline. Puoi rifarlo dopo. + Impossibile sincronizzare uno o più file. Controlla la tua connessione Internet e riprova a inviare. + Avvio download + Impossibile aggiungere dei corsi ai preferiti offline. + Tutti i corsi + Corsi + Gruppi + Tutti i corsi + La selezione dei corsi per Dashboard può essere effettuata solo online. È possibile navigare nei dettagli dei corsi offline. + Nota + Operazione riuscita. Scaricato %1$s di %2$s + Sincronizzare contenuto offline + Elimina notifica + + %d corso in sincronizzazione. + %d corsi in sincronizzazione. + + Immagini contenuto corso + Questo compito non è più disponibile. + Sei offline + Al momento non hai nessun corso disponibile offline. + Sincronizzazione contenuto offline eseguita correttamente + Sincronizzazione contenuto offline non andata a buon fine + Aggiornamenti sincronizzazione offline + Notifiche Canvas per aggiornamenti sincronizzazione offline. + + %d corso è stato sincronizzato. + %d corsi sono stati sincronizzati. + diff --git a/libs/pandares/src/main/res/values-ja/strings.xml b/libs/pandares/src/main/res/values-ja/strings.xml index 0b26bf4991..a64094ca34 100644 --- a/libs/pandares/src/main/res/values-ja/strings.xml +++ b/libs/pandares/src/main/res/values-ja/strings.xml @@ -1373,4 +1373,75 @@ E メール バージョン この課題のリロード中、問題がありました。接続を確認して、もう一度お試しください。 + Instructure ロゴ + 優先設定 + オフラインコンテンツ + 同期 + + + オフラインコンテンツ + オフラインコンテンツを管理する + 保管 + %s / %s を使用 + その他アプリ + Canvas 受講者 + 残り + すべてのコース + 同期化 + %d が選択されました + すべて選択 + すべての選択を取り消し + コンテンツ読み込み中にエラーが起こりました。 + コンテンツの自動同期を有効にすると、選択したコンテンツのダウンロードが以下の設定に基づいて行われます。コンテンツの同期は、アプリケーションが起動していなくても行われます。この設定をオフにすると、同期は行われません。すでにダウンロードされているコンテンツは削除されません。 + 同期の頻度 + コンテンツ自動同期 + コンテンツの同期を行う頻度を指定します。ここで指定した頻度に基づいて、選択したコンテンツをダウンロードします。 + Wi-Fiでのみコンテンツを同期 + この設定を有効にすると、コンテンツの同期はデバイスがWi-Fiネットワークに接続されている場合にのみ行われます(それ以外の場合は、Wi-Fiネットワークが利用可能になるまで延期されます)。 + 同期 + 毎日 + 毎週 + 同期の頻度 + Wi-fiのみでのコンテンツ同期をオフにしますか? + この設定を有効にすると、コンテンツの同期はデバイスがWi-Fiネットワークに接続されている場合にのみ行われます(それ以外の場合は、Wi-Fiネットワークが利用可能になるまで延期されます)。 + オフにする + マニュアル + オフラインモード + オフラインでは利用できません + このコンテンツはオフラインでは利用できません。 + このコンテンツはオフラインでは利用できません。設定を変更したい場合は、ネットワーク利用可能時にダッシュボードからオフラインコンテンツ画面を開いてください。 + オフライン + 同期に失敗しました + %1$s / %2$s ダウンロード中 + キュー + オフラインコンテンツの同期が完了しました + オフラインコンテンツの同期に失敗しました + 同期をキャンセルしますか? + オフラインコンテンツの同期が中止されます。後でやり直すことができます。 + 1つまたは複数のファイルが同期に失敗しました。インターネット接続をチェックしてから、提出を再試行してください。 + ダウンロード開始 + オフラインでコースをお気に入りに追加することはできません。 + すべてのコース + コース + グループ + すべてのコース + ダッシュボードのコース選択はオンラインでのみ可能です。オフラインのコース詳細に移動することができます。 + + 成功!%1$s / %2$s をダウンロード完了 + オフラインコンテンツの同期 + 通知を閉じる + + %dコースは同期中です。 + + コースコンテンツ画像 + この機能はもう利用できません。 + 現在オフラインです + 現在、オフラインで利用可能なコースはありません。 + オフラインコンテンツ同期成功 + オフラインコンテンツの同期失敗 + オフライン同期更新 + オフライン同期化更新の Canvas 通知。 + + %dコースが同期されました。 + diff --git a/libs/pandares/src/main/res/values-mi/strings.xml b/libs/pandares/src/main/res/values-mi/strings.xml index 3ce51d36f9..0dbbc8f33e 100644 --- a/libs/pandares/src/main/res/values-mi/strings.xml +++ b/libs/pandares/src/main/res/values-mi/strings.xml @@ -1391,4 +1391,77 @@ Īmēra Putanga He raru ki te uta ano i tenei taumahi. Tēnā koa tirohia tō hononga ana ka tarai anō. + Tohu Instructure + Ngā manakohanga + Ihirangi tuimotu + Tukutahi + + + Ihirangi tuimotu + Whakahaere ihirangi tuimotu + Rokiroki + %s o %s Kua whakamahia + Ētahi atu Taupānga + Canvas Ākonga + E toe ana + Ngā akoranga katoa + Tukutahi + %d Kua tīpakohia + Tīpako katoa + Whakakorehia te katoa + I puta he hapa i te wa e uta ana te ihirangi. + Ina whakahohea te waahanga Tukutahi Ihirangi Aunoa, ko nga ihirangi kua tikiakehia ka whakawhirinaki ki nga tautuhinga kua whakarārangihia i raro nei. Ahakoa kaore i tuwhera te tono, ka mau tonu te tukutahitanga ihirangi. Karekau he tukutahitanga mena kua weto te tautuhinga. Ko nga mea kua oti te tango ake ka kore e tangohia. + Auautanga Tukutahi + Tukutahi Ihirangi Aunoa + Tohua te maha o nga wa ka puta te tukutahitanga ihirangi. I runga i tenei auau, ka tangohia e te punaha nga korero kua tohua. + Tukutahi Ihirangi Ki runga Wi-Fi Anake + Ko te tukutahitanga ihirangi ka mahi mena ka hono te taputapu ki te whatunga Wi-Fi mena ka whakahohea tenei tautuhinga; ki te kore, ka whakaroa kia watea mai he whatunga Wi-Fi. + Tukutahi + Ia rā + Wiki + Auautanga Tukutahi + Whakawetohia te Tukutahi Ihirangi Ma te Wi-fi Anake? + Ko te tukutahitanga ihirangi ka mahi mena ka hono te taputapu ki te whatunga Wi-Fi mena ka whakahohea tenei tautuhinga; ki te kore, ka whakaroa kia watea mai he whatunga Wi-Fi. + Whakaweto + Ā-ringa + Aratau Tuimotu + Kaore i te waatea tuimotu + Kaore tenei ihirangi i te waatea i te aratau tuimotu. + Kaore tenei ihirangi i te waatea i te aratau tuimotu. Ki te hiahia koe ki te huri i o tautuhinga whakatuwheratia te mata Ihirangi Tuimotu mai i te papatohu ina waatea te whatunga. + Tuimotu + I Rahua te Tukutahi + Tikiake ana %1$s o %2$s + Rārangi + Kua Oti te Tukutahi Ihirangi Tuimotu + I Rahua te Tukutahi Ihirangi Tuimotu + Whakakore Tukutahi? + Ka mutu te tukutahi ihirangi tuimotu. Ka taea e koe te mahi ano i muri mai. + Kotahi neke atu ranei nga konae i rahua ki te tukutahi. Tirohia to hononga ipurangi ka ngana ano ki te tuku. + Ka timata te tango + Kaore e taea te taapiri i nga akoranga ki nga makau tuimotu. + Ngā akoranga katoa + Ngā Akoranga + Rōpū + Ngā akoranga katoa + Ko te whiriwhiri akoranga mo te Papatohu ka taea anake te mahi i runga ipurangi. Ka taea e koe te whakatere ki nga taipitopito akoranga tuimotu. + Tuhipoka + Angitu! Kua tikiakehia %1$s o %2$s + Tukutahi Ihirangi Tuimotu + Whakakorehia te panui + + %d Kei te tukutahi te akoranga. + %d kei te tukutahi nga akoranga. + + Whakaahua ihirangi akoranga + Kaore tēnei whakataunga i te wātea. + Kei te tuimotu koe + I tenei wa karekau he akoranga kei te waatea tuimotu. + Angitu Tukutahi Ihirangi Tuimotu + I Rahua te Tukutahi Ihirangi Tuimotu + Whakahōu Tukutahi Tuimotu + Nga whakamohiotanga canvas mo nga whakahou tukutahi tuimotu. + + %d kua tukutahia te akoranga. + %d kua tukutahia nga akoranga. + diff --git a/libs/pandares/src/main/res/values-ms/strings.xml b/libs/pandares/src/main/res/values-ms/strings.xml index bce19c3d3a..7848518aa7 100644 --- a/libs/pandares/src/main/res/values-ms/strings.xml +++ b/libs/pandares/src/main/res/values-ms/strings.xml @@ -1397,4 +1397,77 @@ E-mel Versi Terdapat sedikit masalah untuk memuat semula tugasan ini. Sila semak sambungan anda dan cuba semula. + Logo Instructure + Keutamaan + Kandungan Luar Talian + Penyegerakan + + + Kandungan Luar Talian + Urus Kandungan Luar Talian + Storan + %s daripada %s Digunakan + Aplikasi Lain + Canvas Student + Berbaki + Semua Kursus + Segerakkan + %d Dipilih + Pilih Semua + Nyahpilih Semua + Ralat berlaku semasa memuatkan kandungan. + Mendayakan Segerakan Kandungan Automatik akan mengurus muat turun kandungan yang dipilih berdasarkan tetapan di bawah. Penyegerakan kandungan akan etap berlaku walaupun aplikasi tidak berjalan. Jika tetapan dimatikan maka tiada penyegerakan akan berlaku. Kandungan yang sudah dimuat turun tidak akan dipadamkan. + Kekerapan Penyegerakan + Segerakan Kandungan Automatik + Tentukan ulangan penyegerakan kandungan Sistem akan memuat turun kandungan yang dipilih berdasarkan kekerapan yang ditentukan di sini. + Segerakkan Kandungan Menerusi Wi-Fi Sahaja + Jika tetapan ini didayakan penyegerakan kandungan hanya akan berlaku jika peranti terhubung dengan rangkaian Wi-Fi, jika tidak, ia akan ditangguhkan sehingga rangkaian Wi-Fi tersedia. + Penyegerakan + Harian + Mingguan + Kekerapan Penyegerakan + Matikan Penyegerakan Kandungan Melalui Wi-fi Sahaja? + Jika tetapan ini didayakan penyegerakan kandungan hanya akan berlaku jika peranti terhubung dengan rangkaian Wi-Fi, jika tidak, ia akan ditangguhkan sehingga rangkaian Wi-Fi tersedia. + Matikan + Manual + Mod Luar Talian + Tidak Tersedia di Luar Talian + Kandungan ini tidak tersedia dalam mod luar talian. + Kandungan ini tidak tersedia dalam mod luar talian. Jika anda ingin mengubah tetapan anda, buka skrin Kandungan Luar Talian daripada papan pemuka apabila rangkaian tersedia. + Luar talian + Penyegerakan Gagal + Memuat turun %1$s daripada %2$s + Dibaris gilir + Penyegerakan Kandungan Luar Talian Lengkap + Penyegerakan Kandungan Luar Talian Gagal + Batal Penyegerakan? + Tindakan ini akan memberhentikan penyegerakan kandungan luar talian. Anda boleh melakukan penyegerakan lagi kemudian. + Satu atau lebih fail gagal disegerakkan. Semak sambungan Internet anda dan cuba semula untuk membuat serahan. + Muat turun bermula + Kursus tidak boleh ditambahkan ke kegemaran semasa di luar talian. + Semua Kursus + Kursus + Kumpulan + Semua Kursus + Memilih kursus untuk Papan Pemuka hanya boleh dilakukan dalam talian. Anda boleh menavigasi ke butiran kursus luar talian. + Nota + Berjaya! Telah memuat turun %1$s daripada %2$s + Menyegerakkan Kandungan Luar Talian + Tolak pemberitahuan + + %d Kursus sedang disegerakkan. + %d Kursus sedang disegerakkan. + + Imej kandungan kursus + Tugasan ini tidak lagi tersedia. + Anda berada di luar talian + Anda tidak mempunyai apa-apa kursus yang tersedia di luar talian buat masa ini. + Berjaya Menyegerakkan Kandungan Luar Talian + Penyegerakan Kandungan Luar Talian Gagal + Kemas kini Penyegerakan Luar Talian + Pemberitahuan Canvas untuk kemas kini penyegerakan luar talian. + + %d Kursus telah disegerakkan. + %d Kursus telah disegerakkan. + diff --git a/libs/pandares/src/main/res/values-nb/strings.xml b/libs/pandares/src/main/res/values-nb/strings.xml index 74ed00f0d0..81e7b55592 100644 --- a/libs/pandares/src/main/res/values-nb/strings.xml +++ b/libs/pandares/src/main/res/values-nb/strings.xml @@ -1392,4 +1392,77 @@ E-post Versjon Det oppstod et problem med lasting av denne oppgaven. Kontroller tilkoblingen og prøv på nytt. + Instructure-logo + Preferanser + Frakoblet emneinnhold + Synkronisering + + + Frakoblet emneinnhold + Administrer frakoblet emneinnhold + Lagring + %s av %s brukt + Andre apper + Canvas-student + Resterende + Alle åpne emner + Synkroniser + %d er valgt + Velg alle + Fjern all merking + Det oppsto en feil ved lasting av innholdet. + Aktivering av Automatisk synkronisering av innhold vil ta seg av nedlasting av det valgte innholdet basert på følgende innstillinger. Synkronisering av innhold vil skje selv om applikasjonen ikke kjører. Hvis innstillingen er slått av, vil det ikke skje noen synkronisering. Det allerede nedlastede innholdet vil ikke bli slettet. + Synkroniseringsfrekvens + Automatisk synkronisering av innhold + Spesifiser gjentakelsen av innholdssynkroniseringen. Systemet vil laste ned det valgte innholdet basert på frekvensen som er spesifisert her. + Synkroniser innhold kun over Wi-Fi + Hvis denne innstillingen er aktivert, vil innholdssynkroniseringen bare skje hvis enheten er koblet til et Wi-Fi-nettverk. Hvis ikke, vil det bli utsatt til et Wi-Fi-nettverk er tilgjengelig. + Synkronisering + Daglig + Ukentlig + Synkroniseringsfrekvens + Slå av innholdssynkronisering kun over Wi-Fi? + Hvis denne innstillingen er aktivert, vil innholdssynkroniseringen bare skje hvis enheten er koblet til et Wi-Fi-nettverk. Hvis ikke, vil det bli utsatt til et Wi-Fi-nettverk er tilgjengelig. + Slå av + Manuell + Frakoblet modus + Ikke tilgjengelig i frakoblet modus + Innholdet er ikke tilgjengelig i frakoblet modus. + Innholdet er ikke tilgjengelig i frakoblet modus. Hvis du vil endre innstillingene dine, åpne skjermen Frakoblet emneinnhold fra dashbordet når du er koblet til internett. + Frakoblet + Synkronisering mislyktes + Laster ned %1$s av %2$s + Satt i kø + Synkronisering av frakoblet emneinnhold fullført + Synkronisering av frakoblet emneinnhold mislyktes + Avbryte synkronisering? + Det vil stoppe synkronisering av frakoblet emneinnhold Du kan gjøre det igjen senere. + Én eller flere filer kunne ikke synkroniseres. Sjekk internettforbindelsen din og prøv å lever på nytt. + Nedlasting startere + Emner kan ikke legges til i favoritter i frakoblet modus. + Alle åpne emner + Emner + Grupper + Alle åpne emner + Å velge emner for dashbord kan bare gjøres når du er tilkoblet. Du kan navigere til emnedetaljer i frakoblet modus. + Merknad + Vellykket! Lastet ned %1$s av %2$s + Synkroniserer frakoblet innhold + Avvis varsling + + %d emne synkroniseres. + %d emner synkroniseres. + + Emneinnhold-bilder + Denne oppgaven er ikke lenger tilgjengelig. + Du er frakoblet + Du har ingen emner som er tilgjengelig i frakoblet modus. + Synkronisering av frakoblet emneinnhold vellykket + Synkronisering av frakoblet emneinnhold mislyktes + Synkronisering av oppdateringer i frakoblet modus + Canvas-varslinger for synkronisering av oppdateringer i frakoblet modus. + + %d emne er synkronisert. + %d emner er synkronisert. + diff --git a/libs/pandares/src/main/res/values-nl/strings.xml b/libs/pandares/src/main/res/values-nl/strings.xml index 0b32e79400..182e1a3bac 100644 --- a/libs/pandares/src/main/res/values-nl/strings.xml +++ b/libs/pandares/src/main/res/values-nl/strings.xml @@ -1391,4 +1391,77 @@ E-mail Versie Er is een probleem met het opnieuw laden van deze opdracht. Controleer je verbinding en probeer het opnieuw. + Logo Instructure + Voorkeuren + Offline inhoud + Synchronisatie + + + Offline inhoud + Offline inhoud beheren + Opslag + %s van %s gebruikt + Andere apps + Canvas Student + Resterend + Alle cursussen + Synchroniseren + %d geselecteerd + Alles selecteren + Selectie van alle items opheffen + Er is een fout opgetreden tijdens het laden van de content. + Door Automatisch synchroniseren van content in te schakelen wordt de geselecteerde content gedownload op basis van de onderstaande instellingen. De contentsynchronisatie vindt ook plaats als de applicatie niet wordt uitgevoerd. Als de instelling wordt uitgeschakeld, wordt er geen synchronisatie uitgevoerd. De reeds gedownloade content wordt niet verwijderd. + Synchronisatiefrequentie + Automatisch synchroniseren van content + Geef de herhalingsfactor van de contentsynchronisatie op. Het systeem downloadt de geselecteerde content op basis van de hier opgegeven frequentie. + Content alleen synchroniseren via wifi + Als deze instelling wordt ingeschakeld, vindt contentsynchronisatie alleen plaats als het apparaat verbonden is met een wifi-netwerk, anders wordt de synchronisatie uitgesteld totdat er een wifi-netwerk beschikbaar is. + Synchronisatie + Dagelijks + Wekelijks + Synchronisatiefrequentie + Content alleen synchroniseren via wifi uitschakelen? + Als deze instelling wordt ingeschakeld, vindt contentsynchronisatie alleen plaats als het apparaat verbonden is met een wifi-netwerk, anders wordt de synchronisatie uitgesteld totdat er een wifi-netwerk beschikbaar is. + Uitschakelen + Handmatig + Offline modus + Niet offline beschikbaar + Deze content is niet beschikbaar in de offlinemodus. + Deze content is niet beschikbaar in de offlinemodus. Als je je instellingen wilt wijzigen, open je het scherm Offline inhoud op het dashboard wanneer het netwerk beschikbaar is. + Offline + Synchronisatie mislukt. + %1$s van %2$s downloaden + In de wachtrij geplaatst + Offline contentsynchronisatie voltooid + Offline contentsynchronisatie mislukt + Synchronisatie annuleren? + De offline inhoud wordt dan niet meer gesynchroniseerd. U kunt later opnieuw synchroniseren. + Een of meer bestanden kunnen niet worden gesynchroniseerd. Controleer je internetverbinding en probeer opnieuw in te leveren. + Bezig met downloaden + Cursussen kunnen niet offline worden toegevoegd aan favorieten. + Alle cursussen + Cursussen + Groepen + Alle cursussen + Cursussen voor Dashboard kunnen alleen online worden geselecteerd. Je kunt naar offline cursusgegevens. + Opmerking + Gelukt! %1$s van %2$s gedownload + Offline inhoud synchroniseren + Melding verwijderen + + %d-cursus wordt gesynchroniseerd. + %d-cursussen worden gesynchroniseerd. + + Afbeeldingen van cursusinhoud + Deze opdracht is niet meer beschikbaar. + Je bent offline + Je hebt momenteel geen cursussen die offline beschikbaar zijn. + Offline contentsynchronisatie geslaagd + Offline contentsynchronisatie mislukt + Offline synchronisatie-updates + Canvas-meldingen voor offline synchronisatie-updates. + + %d-cursus is gesynchroniseerd. + %d-cursussen zijn gesynchroniseerd. + diff --git a/libs/pandares/src/main/res/values-pl/strings.xml b/libs/pandares/src/main/res/values-pl/strings.xml index a9f3d91551..2a0f55ce2f 100644 --- a/libs/pandares/src/main/res/values-pl/strings.xml +++ b/libs/pandares/src/main/res/values-pl/strings.xml @@ -1427,4 +1427,81 @@ E-mail Wersja Wystąpił problem z wczytaniem tego zadania. Sprawdź połączenie i spróbuj ponownie. + Logo Instructure + Preferencje + Zawartość offline + Synchronizacja + + + Zawartość offline + Zarządzaj zawartością offline + Magazyn danych + %s z %s użytych + Inne aplikacje + Uczestnik Canvas + Pozostało + Wszystkie kursy + Synchronizacja + %d wybrano + Zaznacz wszystko + Usuń zaznaczenie wszystkich + Podczas wczytywania zawartości wystąpił błąd. + Włączenie automatycznej synchronizacji zawartości pozwoli pobierać wybraną zawartość w oparciu o poniższe ustawienia. Synchronizacja zawartości będzie się odbywać, nawet jeśli aplikacja nie zostanie włączona. Jeśli funkcja jest wyłączona, synchronizacja nie będzie działać. Pobrana już zawartość nie zostanie usunięta. + Częstotliwość synchronizacji + Automatyczna synchronizacja zawartości + Określ występowanie synchronizacji zawartości. System będzie pobierać wybraną zawartość w oparciu o określoną w tym miejscu częstotliwość. + Synchronizuj zawartość tylko przez sieć Wi-Fi + Jeśli funkcja jest włączona, synchronizacja zawartości będzie odbywać się tylko wtedy, gdy urządzenie nawiąże połączenie z siecią Wi-Fi, w przeciwnym razie będzie aktywna dopiero po połączeniu się z siecią Wi-Fi. + Synchronizacja + Codziennie + Co tydzień + Częstotliwość synchronizacji + Wyłączyć synchronizację zawartości tylko przez Wi-Fi? + Jeśli funkcja jest włączona, synchronizacja zawartości będzie odbywać się tylko wtedy, gdy urządzenie nawiąże połączenie z siecią Wi-Fi, w przeciwnym razie będzie aktywna dopiero po połączeniu się z siecią Wi-Fi. + Wyłącz + Ręczna + Tryb offline + Niedostępny offline + Ta zawartość nie jest dostępna w trybie offline. + Ta zawartość nie jest dostępna w trybie offline. Aby zmienić ustawienia, otwórz ekran zawartości offline z pulpitu nawigacyjnego, gdy sieć stanie się dostępna. + Offline + Niepowodzenie synchronizacji + Pobieranie %1$s z %2$s + W kolejce + Zakończono synchronizację zawartości offline + Niepowodzenie synchronizacji zawartości offline + Anulować synchronizację? + Zatrzyma synchronizację zawartości offline. Można to zrobić ponownie później. + Nie udało się zsynchronizować co najmniej jednego pliku. Sprawdź połączenie internetowe i ponów przesyłanie. + Rozpoczynanie pobierania + Kursów nie można dodać do ulubionych offline. + Wszystkie kursy + Kursy + Grupy + Wszystkie kursy + Kursy dla pulpitu nawigacyjnego można wybrać tylko online. Można przejść do szczegółów kursu offline. + Uwaga + Zakończono powodzeniem! Pobrano %1$s z %2$s + Synchronizowanie zawartości offline + Odrzuć powiadomienie + + Trwa synchronizacja kursu %d. + Trwa synchronizacja kursów %d. + Trwa synchronizacja kursów %d. + Trwa synchronizacja kursów %d. + + Obrazy zawartości kursu + To zadanie nie jest już dostępne. + Jesteś offline + Obecnie nie masz żadnych kursów dostępnych offline. + Synchronizacja treści offline zakończona pomyślnie + Niepowodzenie synchronizacji zawartości offline + Aktualizacje synchronizacji offline + Powiadomienia Canvas dla aktualizacji synchronizacji offline. + + Kurs %d został zsynchronizowany. + Kursy %d zostały zsynchronizowane. + Kursy %d zostały zsynchronizowane. + Kursy %d zostały zsynchronizowane. + diff --git a/libs/pandares/src/main/res/values-pt-rBR/strings.xml b/libs/pandares/src/main/res/values-pt-rBR/strings.xml index dc5f3aec5c..743fe896ee 100644 --- a/libs/pandares/src/main/res/values-pt-rBR/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rBR/strings.xml @@ -1391,4 +1391,77 @@ E-mail Versão Houve um problema ao recarregar esta tarefa. Verifique a sua conexão e tente novamente. + Logotipo Instructure + Preferências + Conteúdo off-line + Sincronização + + + Conteúdo off-line + Gerenciar conteúdo off-line + Armazenamento + %s de %s usado + Outros aplicativos + Canvas Student + Restante + Todos os cursos + Sincronizar + %d Selecionado(s) + Selecionar tudo + Cancelar seleção de todos + Ocorreu um erro ao carregar o conteúdo. + A ativação da sincronização automática de conteúdo cuidará do download do conteúdo selecionado com base nas configurações abaixo. A sincronização de conteúdo acontecerá mesmo se o aplicativo não estiver em execução. Se a configuração estiver desativada, nenhuma sincronização ocorrerá. O conteúdo já baixado não será excluído. + Frequência de sincronização + Sincronização automática de conteúdo + Especifique a recorrência da sincronização de conteúdo. O sistema fará o download do conteúdo selecionado com base na frequência especificada aqui. + Sincronizar conteúdo apenas por Wi-Fi + Se esta configuração estiver habilitada, a sincronização de conteúdo só acontecerá se o dispositivo se conectar a uma rede Wi-Fi, caso contrário, será adiada até que uma rede Wi-Fi esteja disponível. + Sincronização + Diariamente + Semanalmente + Frequência de sincronização + Desativar a sincronização de conteúdo somente por Wi-Fi? + Se esta configuração estiver habilitada, a sincronização de conteúdo só acontecerá se o dispositivo se conectar a uma rede Wi-Fi, caso contrário, será adiada até que uma rede Wi-Fi esteja disponível. + Desativar + Manual + Modo offline + Não disponível off-line + Este conteúdo não está disponível no modo offline. + Este conteúdo não está disponível no modo offline. Se você quiser alterar suas configurações, abra a tela Conteúdo Off-line no painel quando a rede estiver disponível. + Offline + Sincronização falhou + Baixando %1$s de %2$s + Enfileirado + Sincronização de conteúdo off-line concluída + Falha na sincronização de conteúdo off-line + Cancelar sincronização? + Isso interromperá a sincronização de conteúdo offline. Você pode fazer isso de novo mais tarde. + Falha na sincronização de um ou mais arquivos. Verifique sua conexão à Internet e tente enviar novamente. + Download começando + Os cursos não podem ser adicionados aos favoritos offline. + Todos os cursos + Cursos + Grupos + Todos os cursos + A seleção de cursos para o Painel só pode ser feita online. Você pode navegar até os detalhes do curso off-line. + Observação + Sucesso! Baixado %1$s de %2$s + Sincronizando conteúdo off-line + Descartar notificação + + %d curso está sendo sincronizado. + %d cursos estão sendo sincronizados. + + Imagens do conteúdo do curso + Essa tarefa não está mais disponível. + Você está off-line + No momento, você não tem nenhum curso disponível off-line. + Sucesso na sincronização de conteúdo off-line + Falha na sincronização de conteúdo off-line + Atualizações de sincronização off-line + Notificações do Canvas para atualizações de sincronização offline. + + %d curso foi sincronizado. + %d cursos foram sincronizados. + diff --git a/libs/pandares/src/main/res/values-pt-rPT/strings.xml b/libs/pandares/src/main/res/values-pt-rPT/strings.xml index 1bf5144981..0e9fe3c275 100644 --- a/libs/pandares/src/main/res/values-pt-rPT/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rPT/strings.xml @@ -1391,4 +1391,77 @@ E-mail Versão Ocorreu um problema ao recarregar esta atribuição. É favor verificar sua conexão e tente novamente. + Logótipo da Instructure + Preferências + Conteúdo offline + Sincronização + + + Conteúdo offline + Gerir conteúdo offline + Armazenamento + %s de %s Utilizado + Outras aplicações + Aluno Canvas + Restante + Todas as disciplinas + Sincronizar + %d Selecionado + Selecionar tudo + Desmarcar todos + Ocorreu um erro ao carregar o conteúdo. + A ativação da Sincronização automática de conteúdos encarregar-se-á de descarregar o conteúdo selecionado com base nas definições abaixo. A sincronização de conteúdos ocorrerá mesmo que a aplicação não esteja a ser executada. Se a definição estiver desativada, não será efetuada qualquer sincronização. O conteúdo já transferido não será eliminado. + Frequência de sincronização + Sincronização automática de conteúdos + Especifique a recorrência da sincronização de conteúdos. O sistema irá transferir o conteúdo selecionado com base na frequência aqui especificada. + Sincronizar conteúdo apenas por Wi-Fi + Se esta definição estiver ativada, a sincronização de conteúdos só será efetuada se o dispositivo estiver ligado a uma rede Wi-Fi; caso contrário, será adiada até estar disponível uma rede Wi-Fi. + Sincronização + Diariamente + Semanalmente + Frequência de sincronização + Desativar a sincronização de conteúdos apenas através de Wi-fi? + Se esta definição estiver ativada, a sincronização de conteúdos só será efetuada se o dispositivo estiver ligado a uma rede Wi-Fi; caso contrário, será adiada até estar disponível uma rede Wi-Fi. + Desligar + Manual + Modo off-line + Não disponível em modo off-line + Este conteúdo não está disponível no modo off-line. + Este conteúdo não está disponível no modo off-line. Se pretender alterar as suas definições, abra o ecrã Conteúdo off-line a partir do painel de instrumentos quando a rede estiver disponível. + Off-line + Falha na sincronização + Descarregar %1$s de %2$s + Em fila de espera + Sincronização de conteúdos off-line concluída + Falha na sincronização de conteúdos offline + Cancelar sincronização? + A sincronização de conteúdos offline será interrompida. Pode voltar a fazê-lo mais tarde. + Um ou mais ficheiros não foram sincronizados. Verifique sua conexão com a Internet e tente enviar novamente. + Início da transferência + Os cursos não podem ser adicionados aos favoritos off-line. + Todas as disciplinas + Disciplinas + Grupos + Todas as disciplinas + A seleção de cursos para o Painel de controlo só pode ser feita on-line. Pode navegar para os detalhes da disciplina off-line. + Observação + Sucesso! Transferido %1$s de %2$s + Sincronizar conteúdo off-line + Ignorar notificação + + %d a disciplina está a sincronizar-se. + %d as disciplinas estão a sincronizar-se. + + Imagens do conteúdo da disciplina + Esta tarefa não está mais disponível. + Está offline + Atualmente, não tem quaisquer disciplinas disponíveis offline. + Sucesso na sincronização de conteúdo off-line + Falha na sincronização de conteúdos offline + Atualizações da sincronização offline + Notificações do Canvas para actualizações de sincronização offline. + + %d a disciplina foi sincronizada. + %d as disciplinas foram sincronizadas. + diff --git a/libs/pandares/src/main/res/values-ru/strings.xml b/libs/pandares/src/main/res/values-ru/strings.xml index 9c7d3ded27..5130909e65 100644 --- a/libs/pandares/src/main/res/values-ru/strings.xml +++ b/libs/pandares/src/main/res/values-ru/strings.xml @@ -1427,4 +1427,81 @@ Адрес электронной почты Версия Возникла проблема с перезагрузкой этого задания. Проверьте подключение и попробуйте еще раз. + Логотип Instructure + Предпочтения + Оффлайн-контент + Синхронизация + + + Оффлайн-контент + Управление офлайн-контентом + Хранилище + %s из %s использовано + Другие приложения + Студент Canvas + Остается + Все курсы + Синхронизировать + %d выбрано + Выбрать все + Отменить выбор для всех + Произошла ошибка при загрузке контента. + При включении функции автоматической синхронизации содержимого будет выполнена загрузка выбранного содержимого на основе приведенных ниже настроек. Синхронизация содержимого будет происходить, даже если приложение не запущено. Если этот параметр выключен, синхронизация не будет выполняться. Уже загруженное содержимое не будет удаляться. + Синхронизация частоты + Автоматическая синхронизация контента + Укажите периодичность синхронизации содержимого. Система будет скачивать выбранное содержимое в соответствии с указанной здесь периодичностью. + Синхронизация содержимого только по Wi-Fi + Если этот параметр включен, синхронизация содержимого будет происходить только при подключении устройства к сети Wi-Fi. В противном случае она будет отложена до появления доступной сети Wi-Fi. + Синхронизация + Ежедневно + Еженедельно + Синхронизация частоты + Отключить синхронизацию содержимого только через Wi-Fi? + Если этот параметр включен, синхронизация содержимого будет происходить только при подключении устройства к сети Wi-Fi. В противном случае она будет отложена до появления доступной сети Wi-Fi. + Отключить + Ручное + Режим офлайн + Недоступно в режиме офлайн + Данное содержимое недоступно в автономном режиме. + Данное содержимое недоступно в автономном режиме. Если вы хотите изменить настройки, откройте окно Автономное содержимое из панели управления, когда сеть доступна. + Не в сети + Синхронизация не выполнена + Скачивание %1$s из %2$s + Запрошено + Синхронизация автономного содержимого завершена + Сбой синхронизации автономного содержимого + Отменить синхронизацию? + Это приведет к остановке синхронизации офлайн-контента. Вы можете сделать это еще раз позднее. + Не удалось синхронизировать один или более файлов. Проверьте подключение к Интернету и повторите отправку. + Скачивание запускается + Курсы не могут быть добавлены в избранное в автономном режиме. + Все курсы + Курсы + Группы + Все курсы + Выбор курсов для Информационной панели может быть выполнен только в режиме онлайн. Вы можете перейти к информации об автономном курсе. + Примечание + Успешно! Скачано %1$s из %2$s + Синхронизировать офлайн-контент + Пропустить уведомление + + %d курс синхронизируется. + %d курса(-ов) синхронизируются. + %d курса(-ов) синхронизируются. + %d курса(-ов) синхронизируются. + + Изображения содержимого курса + Это задание более недоступно. + Вы находитесь в автономном режиме + В настоящее время у вас нет никаких курсов, доступных в автономном режиме. + Синхронизация автономного содержимого успешно выполнена + Сбой синхронизации автономного содержимого + Обновления автономной синхронизации + Уведомления Canvas для обновлений синхронизации в автономном режиме. + + %d курс был синхронизирован. + %d курса(-ов) были синхронизированы. + %d курса(-ов) были синхронизированы. + %d курса(-ов) были синхронизированы. + diff --git a/libs/pandares/src/main/res/values-sl/strings.xml b/libs/pandares/src/main/res/values-sl/strings.xml index b77f4e12fc..c5e6bbde77 100644 --- a/libs/pandares/src/main/res/values-sl/strings.xml +++ b/libs/pandares/src/main/res/values-sl/strings.xml @@ -1391,4 +1391,77 @@ E-pošta Različica Prišlo je do težave z ponovnim nalaganjem te naloge. Preverite svojo povezavo in poskusite znova. + Logotip Instructure + Nastavitve + Vsebina brez povezave + Sinhronizacija + + + Vsebina brez povezave + Upravljajte vsebino brez povezave + Shramba + %s od %s uporabljenih + Druge aplikacije + Študent v sistemu Canvas + Preostalo + Vsi predmeti + Sinhronizacija + Izbrana je možnost %d + Izberi vse + Razveljavi izbor vseh + Pri nalaganju vsebine je prišlo do napake. + Če omogočite samodejno sinhronizacijo vsebine, boste omogočili prenos izbrane vsebine na podlagi spodnjih nastavitev. Sinhronizacija vsebine bo izvedena tudi, če aplikacija ni zagnana. Če je nastavitev izklopljena, sinhronizacija ne bo izvedena. Že prenesena vsebina ne bo odstranjena. + Pogostost sinhronizacije + Samodejna sinhronizacija vsebine + Določite ponavljanje sinhronizacije vsebine. Sistem bo prenesel izbrano vsebino glede na tukaj določeno pogostost. + Sinhroniziraj vsebino samo prek Wi-Fi + Če je ta nastavitev omogočena, se bo sinhronizacija vsebine zgodila le, če se naprava poveže z omrežjem Wi-Fi, sicer bo odložena, dokler omrežje Wi-Fi ne bo na voljo. + Sinhronizacija + Dnevno + Tedensko + Pogostost sinhronizacije + Želite izklopiti sinhronizacijo vsebine samo prek Wi-Fi? + Če je ta nastavitev omogočena, se bo sinhronizacija vsebine zgodila le, če se naprava poveže z omrežjem Wi-Fi, sicer bo odložena, dokler omrežje Wi-Fi ne bo na voljo. + Izklop + Ročno + Način brez povezave + Ni na voljo brez povezave + Ta vsebina ni na voljo v načinu brez povezave. + Ta vsebina ni na voljo v načinu brez povezave. Če želite spremeniti nastavitve, v preglednici med tem, ko je omrežje na voljo, odprite zaslon Vsebina brez povezave. + Brez povezave + Sinhronizacija ni uspela + Prenos %1$s od %2$s + V čakalni vrsti. + Sinhronizacija vsebine brez povezave je zaključena + Sinhronizacija vsebine brez povezave ni uspela + Želite preklicati sinhronizacijo? + S tem boste zaustavili sinhronizacijo vsebine brez povezave. To lahko pozneje ponovite. + Sinhronizacija ene ali več datotek ni uspela. Preverite internetno povezavo in poskusite poslati znova. + Začetek prenosa + Predmetov ni mogoče dodati med priljubljene, če povezava ni na voljo. + Vsi predmeti + Predmeti + Skupine + Vsi predmeti + Izbira predmetov za preglednico je mogoča samo s spletno povezavo. Odprete lahko podrobnosti o predmetih brez povezave. + Opomba + Uspešno! Preneseno %1$s od %2$s + Sinhronizacija vsebine brez povezave + Opusti obvestilo + + %d predmet se sinhronizira. + %d predmetov se sinhronizira. + + Slike vsebine predmeta + Ta naloga ni več na voljo. + Nimate povezave + Trenutno nimate nobenega predmeta, ki bi bil na voljo brez povezave. + Sinhronizacija vsebine brez povezave je uspela + Sinhronizacija vsebine brez povezave ni uspela + Posodobitve sinhronizacije brez povezave + Obvestila Canvas za posodobitve sinhronizacije brez povezave. + + %d predmet je bil sinhroniziran. + %d predmetov je bilo sinhroniziranih. + diff --git a/libs/pandares/src/main/res/values-sv/strings.xml b/libs/pandares/src/main/res/values-sv/strings.xml index fc1aee1a9d..ae0a2f612e 100644 --- a/libs/pandares/src/main/res/values-sv/strings.xml +++ b/libs/pandares/src/main/res/values-sv/strings.xml @@ -1391,4 +1391,77 @@ E-post Version Det uppstod ett problem när den här uppgiften skulle laddas om. Kontrollera din anslutning och försök igen. + Instructure-logotyp + Inställningar + Offlineinnehåll + Synkronisering + + + Offlineinnehåll + Hantera offlineinnehåll + Lagring + %s av %s har använts + Andra appar + Canvas-student + Återstående + Alla kurser + Synkronisera + %d Vald + Välj alla + Avmarkera alla + Ett fel uppstod vid inläsning av innehållet. + Om automatisk innehållssynkronisering aktiveras laddas det valda innehållet ned baserat på nedanstående inställningar. Innehållssynkroniseringen sker även om programmet inte körs. Om inställningen är avstängd sker ingen synkronisering. Innehåll som redan laddats ned raderas inte. + Synkroniseringsfrekvens + Automatisk innehållssynkronisering + Ange hur ofta innehållssynkroniseringen ska ske. Systemet kommer att ladda ned det valda innehållet baserat på den frekvens du anger här. + Synkronisera endast innehåll över Wi-Fi + Om den här inställningen aktiveras sker innehållssynkroniseringen endast om enheten ansluter till ett Wi-Fi-nätverk, i annat fall skjuts den upp tills ett Wi-Fi-nätverk är tillgängligt. + Synkronisering + Varje dag + Varje vecka + Synkroniseringsfrekvens + Stäng av Synkronisera endast innehåll över Wi-Fi? + Om den här inställningen aktiveras sker innehållssynkroniseringen endast om enheten ansluter till ett Wi-Fi-nätverk, i annat fall skjuts den upp tills ett Wi-Fi-nätverk är tillgängligt. + Stäng av + Manuell + Offlineläge + Inte tillgänglig offline + Det här innehållet är inte tillgängligt i offlineläge. + Det här innehållet är inte tillgängligt i offlineläge. Om du vill ändra dina inställningar öppnar du skärmen Offlineinnehåll i översikten när nätverket är tillgängligt. + Offline + Synkroniseringen misslyckades + Laddar ned %1$s av %2$s + I kö + Innehållssynkronisering offline slutfördes + Innehållssynkronisering offline misslyckades + Avbryta synkroniseringen? + Det stoppar synkronisering av offlineinnehåll. Du kan göra detta vid ett senare tillfälle. + En eller fler filer synkroniserades inte. Kontrollera din internetanslutning och försök lämna in igen. + Nedladdningen startar + Det går inte att lägga till kurser i favoriter offline. + Alla kurser + Kurser + Grupper + Alla kurser + Du kan endast välja kurser till översikten online. Du kan navigera till information om offlinekurser. + Anteckning + Framgång! Laddade ned %1$s av %2$s + Synkroniserar offlineinnehåll + Avvisa aviseringen + + %d-kurs synkroniserar. + %d-kurser synkroniserar. + + Bilder i kursinnehållet + Denna uppgift är inte längre tillgänglig. + Du är offline + Du har för närvarande inte några kurser som är tillgängliga offline. + Offlineinnehåll har synkroniserats + Innehållssynkronisering offline misslyckades + Uppdateringar för offlinesynkronisering + Canvas-aviseringar för uppdateringar för offlinesynkronisering. + + %d-kurs har synkroniserats. + %d-kurser har synkroniserats. + diff --git a/libs/pandares/src/main/res/values-th/strings.xml b/libs/pandares/src/main/res/values-th/strings.xml index e9871f3baf..6023ee0943 100644 --- a/libs/pandares/src/main/res/values-th/strings.xml +++ b/libs/pandares/src/main/res/values-th/strings.xml @@ -1391,4 +1391,77 @@ อีเมล เวอร์ชั่น มีปัญหาในการรีโหลดภารกิจนี้ กรุณาตรวจสอบการเชื่อมต่อของคุณและลองใหม่อีกครั้ง + โลโก้ Instructure + ค่าปรับตั้ง + เนื้อหาออฟไลน์ + การซิงค์ข้อมูล + + + เนื้อหาออฟไลน์ + จัดการเนื้อหาออฟไลน์ + พื้นที่จัดเก็บ + %s จาก %s ที่ใช้ + แอพอื่น ๆ + Canvas Student + ที่เหลือ + บทเรียนทั้งหมด + ซิงค์ + เลือก %d ไว้ + เลือกทั้งหมด + ยกเลิกการเลือกทั้งหมด + เกิดข้อผิดพลาดขณะโหลดเนื้อหา + การเปิดใช้การซิงค์เนื้อหาอัตโนมัติจะเป็นการจัดการการดาวน์โหลดเนื้อหาที่เลือกตามค่าปรับตั้งต่อไปนี้ การซิงค์เนื้อหาจะเกิดขึ้นแม้ว่าแอพพลิเคชั่นจะไม่เปิดทำงานอยู่ หากมีการปิดค่านี้ จะไม่มีการซิงค์ข้อมูลเกิดขึ้น เนื้อหาที่ดาวน์โหลดแล้วจะไม่ถูกลบทิ้ง + ความถี่ในการซิงค์ + ซิงค์เนื้อหาอัตโนมัติ + ระบุการซิงค์เนื้อหาซ้ำ ระบบจะดาวนโหลดเนื้อหาที่เลือกตามความถี่ที่ระบุไว้นี้ + ซิงค์เนื้อหาผ่าน Wi-Fi เท่านั้น + หากเปิดใช้งานค่านี้ไว้ การซิงค์เนื้อหาจะเกิดขึ้นก็ต่อเมื่ออุปกรณ์เชื่อมต่อกับเครือข่าย Wi-Fi ไม่เช่นนั้นจะถูกเลื่อนกำหนดจนกว่าเครือข่าย Wi-Fi จะพร้อมใช้งาน + การซิงค์ข้อมูล + รายวัน + รายสัปดาห์ + ความถี่ในการซิงค์ + ปิดการซิงค์เนื้อหาผ่าน Wi-fi เพียงอย่างเดียวหรือไม่ + หากเปิดใช้งานค่านี้ไว้ การซิงค์เนื้อหาจะเกิดขึ้นก็ต่อเมื่ออุปกรณ์เชื่อมต่อกับเครือข่าย Wi-Fi ไม่เช่นนั้นจะถูกเลื่อนกำหนดจนกว่าเครือข่าย Wi-Fi จะพร้อมใช้งาน + ปิด + แมนวล + โหมดออฟไลน์ + ไม่พร้อมใช้งานแบบออฟไลน์ + เนื้อหานี้ไม่สามารถใช้ได้ในโหมดออฟไลน์ + เนื้อหานี้ไม่สามารถใช้ได้ในโหมดออฟไลน์ หากคุณต้องการแก้ไขค่าปรับตั้งของคุณให้เปิดหน้าจอ เนื้อหาออฟไลน์จากแผงข้อมูลเมื่อเครือข่ายพร้อมใช้งาน + ออฟไลน์ + ซิงค์ล้มเหลว + กำลังดาวน์โหลด %1$s จาก %2$s + เข้าคิวแล้ว + ซิงค์เนื้อหาออฟไลน์เสร็จสิ้น + ซิงค์เนื้อหาออฟไลน์ล้มเหลว + ยกเลิกการซิงค์หรือไม่ + การซิงค์ข้อมูลออฟไลน์จะหยุดลง คุณสามารถดำเนินการได้อีกครั้งในภายหลัง + ไฟล์บางส่วนไม่สามารถซิงค์ได้ ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณแล้วลองส่งใหม่อีกครั้ง + เริ่มดาวน์โหลด + ไม่สามารถเพิ่มบทเรียนไปยังรายการโปรดแบบออฟไลน์ + บทเรียนทั้งหมด + บทเรียน + กลุ่ม + บทเรียนทั้งหมด + การเลือกบทเรียนสำหรับแผงข้อมูลสามารถทำได้เมื่อออนไลน์เท่านั้น คุณสามารถดูรายละเอียดบทเรียนแบบออฟไลน์ได้ + หมายเหตุ + เสร็จสิ้น! ดาวน์โหลดแล้ว %1$s จาก %2$s + กำลังซิงค์ข้อมูลออฟไลน์ + ยกเลิกการแจ้งข้อมูล + + %d บทเรียนกำลังซิงค์อยู่ + %d บทเรียนกำลังซิงค์อยู่ + + ภาพเนื้อหาบทเรียน + ภารกิจนี้ไม่มีอยู่อีกต่อไป + คุณออฟไลน์อยู่ + ปัจจุบันคุณไม่มีบทเรียนแบบออฟไลน์ + ซิงค์เนื้อหาออฟไลน์เสร็จสิ้น + ซิงค์เนื้อหาออฟไลน์ล้มเหลว + อัพเดตการซิงค์ออฟไลน์ + การแจ้งข้อมูลจาก Canvas สำหรับการอัพเดตการซิงค์ออฟไลน์ + + %d บทเรียนได้รับการซิงค์แล้ว + %d บทเรียนได้รับการซิงค์แล้ว + diff --git a/libs/pandares/src/main/res/values-vi/strings.xml b/libs/pandares/src/main/res/values-vi/strings.xml index bcf91a4a53..886940b349 100644 --- a/libs/pandares/src/main/res/values-vi/strings.xml +++ b/libs/pandares/src/main/res/values-vi/strings.xml @@ -1392,4 +1392,77 @@ Email Phiên bản Đã xảy ra vấn đề khi tải lại bài tập này. Vui lòng kiểm tra kết nối của bạn rồi thử lại. + Logo Instructure + Ưu tiên + Nội Dung Ngoại Tuyến + Đồng bộ hóa + + + Nội Dung Ngoại Tuyến + Quản Lý Nội Dung Ngoại Tuyến + Lưu trữ + %s / %s Đã Sử Dụng + Các Ứng Dụng Khác + Sinh Viên Canvas + Còn lại + Tất Cả Khóa Học + Đồng bộ + Đã Chọn %d + Chọn Tất Cả + Bỏ Chọn Tất Cả + Đã xảy ra lỗi khi tải nội dung. + Bật chức năng Tự Động Đồng Bộ Nội Dung sẽ xử lý việc tải xuống các nội dung được chọn dựa theo cài đặt dưới đây. Thao tác đồng bộ nội dung sẽ diễn ra ngay cả khi ứng dụng không chạy. Nếu tắt cài đặt thì quá trình đồng bộ sẽ không diễn ra. Nội dung đã được tải xuống sẽ không bị xóa. + Tần Suất Đồng Bộ + Tự Động Đồng Bộ Nội Dung + Chỉ định thời gian lặp lại quá trình đồng bộ hóa nội dung. Hệ thống sẽ tải xuống nội dung được chọn dựa theo tần số được chỉ định tại đây. + Chỉ Đồng Bộ Nội Dung Khi Có Wi-Fi + Nếu bật cài đặt thì quá trình đồng bộ hóa nội dung sẽ chỉ diễn ra khi thiết bị kết nối với mạng Wi-Fi, còn không thì quá trình này sẽ bị hoãn lại cho đến khi có mạng Wi-Fi. + Đồng bộ hóa + Hàng ngày + Hằng Tuần + Tần Suất Đồng Bộ + Tắt Đồng Bộ Nội Dung Chỉ Khi Có Wi-fi? + Nếu bật cài đặt thì quá trình đồng bộ hóa nội dung sẽ chỉ diễn ra khi thiết bị kết nối với mạng Wi-Fi, còn không thì quá trình này sẽ bị hoãn lại cho đến khi có mạng Wi-Fi. + Tắt + Thủ công + Chế Độ Ngoại Tuyến + Không Khả Dụng Ngoại Tuyến + Nội dung này không khả dụng ngoại tuyến. + Nội dung này không khả dụng ngoại tuyến. Nếu bạn muốn thay đổi cài đặt của mình, hãy mở màn hình Nội Dung Ngoại Tuyến từ bảng điều khiển khi có mạng. + Ngoại tuyến + Đồng Bộ Không Thành Công + Đang tải xuống %1$s / %2$s + Đã xếp vào hàng chờ + Đã Hoàn Thành Đồng Bộ Nội Dung Ngoại Tuyến + Đồng Bộ Nội Dung Ngoại Tuyến Không Thành Công + Hủy Đồng Bộ? + Việc này sẽ ngừng đồng bộ nội dung ngoại tuyến. Bạn có thể thực hiện lại sau. + Một hoặc nhiều tập tin không đồng bộ thành công. Hãy kiểm tra kết nối internet của bạn rồi thử lại để nộp. + Đang bắt đầu tải xuống + Không thể thêm các khóa học vào mục yêu thích ngoại tuyến. + Tất Cả Khóa Học + Các khóa học + Các nhóm + Tất Cả Khóa Học + Chỉ có thể chọn trực tuyến các khóa học cho Bảng Điều Khiển. Bạn có thể chuyển đến thông tin chi tiết ngoại tuyến về khóa học. + Ghi chú + Thành công! Đã tải xuống %1$s / %2$s + Đang Đồng Bộ Nội Dung Ngoại Tuyến + Bỏ qua thông báo + + %d khóa học đang đồng bộ. + %d khóa học đang đồng bộ. + + Hình ảnh nội dung khóa học + Bài tập này không còn khả dụng. + Bạn đang ngoại tuyến + Hiện tại bạn không có bất kỳ khóa học nào khả dụng ngoại tuyến. + Đồng Bộ Nội Dung Ngoại Tuyến Thành Công + Đồng Bộ Nội Dung Ngoại Tuyến Không Thành Công + Các cập nhật Đồng Bộ Ngoại Tuyến + Thông báo Canvas cho cập nhật đồng bộ ngoại tuyến. + + %d khóa học đã được đồng bộ. + %d khóa học đã được đồng bộ. + diff --git a/libs/pandares/src/main/res/values-zh/strings.xml b/libs/pandares/src/main/res/values-zh/strings.xml index 9fd0ff9c21..f9e4a98069 100644 --- a/libs/pandares/src/main/res/values-zh/strings.xml +++ b/libs/pandares/src/main/res/values-zh/strings.xml @@ -1373,4 +1373,75 @@ 电子邮件 版本 重新加载此作业时出错。请检查连接,然后重试。 + Instructure 徽标 + 首选项 + 离线内容 + 同步 + + + 离线内容 + 管理离线内容 + 存储空间 + %s/%s 个已使用 + 其他应用程序 + Canvas 学生 + 剩余 + 所有课程 + 同步 + %d 已选择 + 全选 + 取消全选 + 加载内容时出错。 + 如果启用“自动同步内容”,将根据以下设置下载所选内容。即使应用程序没有运行,内容也会同步。如果关闭该设置,将不会同步。已下载的内容不会被删除。 + 同步周期 + 自动同步内容 + 指定内容同步的周期。系统将根据此处指定的周期下载所选内容。 + 仅通过无线网络同步内容 + 如果启用该设置,将只在设备连接无线网络的情况下同步内容,否则将推迟到连接无线网络后再同步。 + 同步 + 每天 + 每周 + 同步周期 + 是否关闭仅通过无线网络同步内容? + 如果启用该设置,将只在设备连接无线网络的情况下同步内容,否则将推迟到连接无线网络后再同步。 + 关闭 + 手动 + 离线模式 + 离线时不可用 + 此内容不能在离线模式下使用。 + 此内容不能在离线模式下使用。如需更改设置,请在连接网络后从控制面板打开离线内容屏幕。 + 离线 + 同步失败 + 正在下载 %1$s/%2$s 项 + 已加入队列 + 离线内容同步已完成 + 离线同步内容失败 + 是否取消同步? + 将停止离线同步内容。您可以稍后再次操作。 + 一个或多个文件未能同步。请检查网络连接,并再次尝试提交。 + 正在开始下载 + 无法离线将课程添加到收藏 + 所有课程 + 课程 + 小组 + 所有课程 + 控制面板选择课程只能在离线模式下进行。您可以导航到离线课程详情。 + + 成功!已下载 %1$s/%2$s 项 + 正在同步脱机内容 + 解散通知 + + %d 门课程正在同步。 + + 课程内容图像 + 此作业不再可用。 + 您已离线 + 您目前没有任何可离线使用的课程。 + 离线同步内容成功 + 离线同步内容失败 + 离线同步更新 + Canvas 离线同步更新通知。 + + %d 门课程已同步。 + diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 27c926147c..c301e10fc8 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1432,6 +1432,8 @@ Sync Loading... Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off then no synchronization will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json index 7e8f420426..31ef763ae0 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "f792290082e6cf2f70533d168ec4901b", + "identityHash": "8fa86dcf6c28df2a7e77dee7a0e3c7c6", "entities": [ { "tableName": "AssignmentDueDateEntity", @@ -4709,7 +4709,7 @@ }, { "tableName": "ModuleCompletionRequirementEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -4746,6 +4746,12 @@ "columnName": "moduleId", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -4757,11 +4763,11 @@ "indices": [], "foreignKeys": [ { - "table": "ModuleObjectEntity", + "table": "CourseEntity", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ - "moduleId" + "courseId" ], "referencedColumns": [ "id" @@ -5437,12 +5443,12 @@ }, { "tableName": "FileSyncProgressEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `fileId` INTEGER NOT NULL, PRIMARY KEY(`workerId`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { - "fieldPath": "workerId", - "columnName": "workerId", - "affinity": "TEXT", + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", "notNull": true }, { @@ -5480,18 +5486,12 @@ "columnName": "progressState", "affinity": "TEXT", "notNull": true - }, - { - "fieldPath": "fileId", - "columnName": "fileId", - "affinity": "INTEGER", - "notNull": true } ], "primaryKey": { - "autoGenerate": false, + "autoGenerate": true, "columnNames": [ - "workerId" + "fileId" ] }, "indices": [], @@ -5513,7 +5513,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f792290082e6cf2f70533d168ec4901b')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8fa86dcf6c28df2a7e77dee7a0e3c7c6')" ] } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt index be60845c7a..b412ce5b54 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt @@ -574,4 +574,33 @@ class FileFolderDaoTest { assertEquals(listOf(files[2]), result) } + + @Test + fun testSearchFiles() = runTest { + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + contextId = 1L, + contextType = "Course", + name = "folder", + parentFolderId = 0 + ) + ) + ) + val files = listOf( + FileFolderEntity(FileFolder(id = 2L, displayName = "file1", folderId = 1L)), + FileFolderEntity(FileFolder(id = 3L, displayName = "file2", folderId = 1L)), + FileFolderEntity(FileFolder(id = 4L, displayName = "different name", folderId = 1L)), + FileFolderEntity(FileFolder(id = 5L, displayName = "file hidden", folderId = 1L, isHidden = true)), + FileFolderEntity(FileFolder(id = 6L, displayName = "file hidden for user", folderId = 1L, isHiddenForUser = true)), + ) + + fileFolderDao.insertAll(folders) + fileFolderDao.insertAll(files) + + val result = fileFolderDao.searchCourseFiles(1L, "fil") + + assertEquals(files.subList(0, 2), result) + } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt index 16674d61b0..6f8f1d177d 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt @@ -72,7 +72,6 @@ class FileSyncProgressDaoTest { @Test(expected = SQLiteConstraintException::class) fun testInsertError() = runTest { val entity = FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -89,7 +88,6 @@ class FileSyncProgressDaoTest { @Test(expected = SQLiteConstraintException::class) fun testInsertAllError() = runTest { val entity = FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -108,7 +106,6 @@ class FileSyncProgressDaoTest { courseSyncProgressDao.deleteAll() val entity = FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -122,7 +119,6 @@ class FileSyncProgressDaoTest { @Test fun testForeignKeyDelete() = runTest { val entity = FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -139,40 +135,10 @@ class FileSyncProgressDaoTest { assert(result.isEmpty()) } - @Test - fun testFindByWorkerId() = runTest { - val entities = listOf( - FileSyncProgressEntity( - workerId = "workerId", - courseId = 1L, - fileName = "File 1", - progress = 0, - fileSize = 1000L, - progressState = ProgressState.IN_PROGRESS, - fileId = 1L - ), - FileSyncProgressEntity( - workerId = "workerId2", - courseId = 1L, - fileName = "File 2", - progress = 0, - fileSize = 1000L, - progressState = ProgressState.IN_PROGRESS, - fileId = 1L - ) - ) - fileSyncProgressDao.insertAll(entities) - - val result = fileSyncProgressDao.findByWorkerId("workerId") - - assertEquals(entities[0], result) - } - @Test fun testFindByFileId() = runTest { val entities = listOf( FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -181,7 +147,6 @@ class FileSyncProgressDaoTest { fileId = 1L ), FileSyncProgressEntity( - workerId = "workerId2", courseId = 1L, fileName = "File 2", progress = 0, @@ -198,10 +163,9 @@ class FileSyncProgressDaoTest { } @Test - fun testFindByWorkerIdLiveData() = runTest { + fun testFindByFileIdLiveData() = runTest { val entities = listOf( FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -210,18 +174,17 @@ class FileSyncProgressDaoTest { fileId = 1L ), FileSyncProgressEntity( - workerId = "workerId2", courseId = 1L, fileName = "File 2", progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 2L ) ) fileSyncProgressDao.insertAll(entities) - val result = fileSyncProgressDao.findByWorkerIdLiveData("workerId") + val result = fileSyncProgressDao.findByFileIdLiveData(1L) result.observeForever { } assertEquals(entities[0], result.value) @@ -232,7 +195,6 @@ class FileSyncProgressDaoTest { courseSyncProgressDao.insert(CourseSyncProgressEntity(2L, "workerId2", "Course 2")) val entities = listOf( FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -241,22 +203,20 @@ class FileSyncProgressDaoTest { fileId = 1L ), FileSyncProgressEntity( - workerId = "workerId2", courseId = 1L, fileName = "File 2", progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 2L ), FileSyncProgressEntity( - workerId = "workerId3", courseId = 2L, fileName = "File 3", progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 3L ) ) fileSyncProgressDao.insertAll(entities) @@ -272,7 +232,6 @@ class FileSyncProgressDaoTest { courseSyncProgressDao.insert(CourseSyncProgressEntity(2L, "workerId2", "Course 2")) val entities = listOf( FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -281,22 +240,20 @@ class FileSyncProgressDaoTest { fileId = 1L ), FileSyncProgressEntity( - workerId = "workerId2", courseId = 1L, fileName = "File 2", progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 2L ), FileSyncProgressEntity( - workerId = "workerId3", courseId = 2L, fileName = "File 3", progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 3L ) ) fileSyncProgressDao.insertAll(entities) @@ -311,7 +268,6 @@ class FileSyncProgressDaoTest { fun testDeleteAll() = runTest { val entities = listOf( FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -320,13 +276,12 @@ class FileSyncProgressDaoTest { fileId = 1L ), FileSyncProgressEntity( - workerId = "workerId2", courseId = 1L, fileName = "File 2", progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 2L ) ) @@ -344,7 +299,6 @@ class FileSyncProgressDaoTest { fun testFindAdditionalFilesByCourseIdLiveDataTest() = runTest { val entities = listOf( FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -353,33 +307,30 @@ class FileSyncProgressDaoTest { fileId = 1L ), FileSyncProgressEntity( - workerId = "workerId2", courseId = 1L, fileName = "File 2", progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 2L ), FileSyncProgressEntity( - workerId = "workerId3", courseId = 1L, fileName = "File 3", progress = 0, fileSize = 1000L, additionalFile = true, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 3L ), FileSyncProgressEntity( - workerId = "workerId4", courseId = 1L, fileName = "File 3", progress = 0, fileSize = 0, additionalFile = true, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 4L ) ) @@ -395,7 +346,6 @@ class FileSyncProgressDaoTest { fun testFindCourseFilesByCourseIdLiveData() = runTest { val entities = listOf( FileSyncProgressEntity( - workerId = "workerId", courseId = 1L, fileName = "File 1", progress = 0, @@ -404,33 +354,30 @@ class FileSyncProgressDaoTest { fileId = 1L ), FileSyncProgressEntity( - workerId = "workerId2", courseId = 1L, fileName = "File 2", progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 2L ), FileSyncProgressEntity( - workerId = "workerId3", courseId = 1L, fileName = "File 3", progress = 0, fileSize = 1000L, additionalFile = true, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 3L ), FileSyncProgressEntity( - workerId = "workerId4", courseId = 1L, fileName = "File 3", progress = 0, fileSize = 0, additionalFile = true, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 4L ) ) @@ -441,4 +388,43 @@ class FileSyncProgressDaoTest { assertEquals(entities.subList(0, 2), result.value) } + + @Test + fun testFindByRowId() = runTest { + val entities = listOf( + FileSyncProgressEntity( + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 2L + ) + ) + + fileSyncProgressDao.insertAll(entities) + + val entity = FileSyncProgressEntity( + courseId = 1L, + fileName = "File 3", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 3L + ) + + val rowId = fileSyncProgressDao.insert(entity) + + val result = fileSyncProgressDao.findByRowId(rowId) + + assertEquals(entity, result) + } } diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDaoTest.kt index 4017a2d369..3db57642f1 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDaoTest.kt @@ -68,13 +68,13 @@ class ModuleCompletionRequirementDaoTest { moduleCompletionRequirementDao.insert( ModuleCompletionRequirementEntity( - ModuleCompletionRequirement(id = 1, minScore = 10.0), 1 + ModuleCompletionRequirement(id = 1, minScore = 10.0), 1, 1 ) ) moduleCompletionRequirementDao.insert( ModuleCompletionRequirementEntity( - ModuleCompletionRequirement(id = 2, minScore = 20.0), 2 + ModuleCompletionRequirement(id = 2, minScore = 20.0), 2, 1 ) ) @@ -92,13 +92,13 @@ class ModuleCompletionRequirementDaoTest { moduleCompletionRequirementDao.insert( ModuleCompletionRequirementEntity( - ModuleCompletionRequirement(id = 1, minScore = 10.0), 1 + ModuleCompletionRequirement(id = 1, minScore = 10.0), 1, 1 ) ) moduleCompletionRequirementDao.insert( ModuleCompletionRequirementEntity( - ModuleCompletionRequirement(id = 2, minScore = 20.0), 1 + ModuleCompletionRequirement(id = 2, minScore = 20.0), 1, 1 ) ) @@ -109,19 +109,19 @@ class ModuleCompletionRequirementDaoTest { } @Test(expected = SQLiteConstraintException::class) - fun testModuleItemForeignKey() = runTest { + fun testCourseForeignKey() = runTest { moduleCompletionRequirementDao.insert( - ModuleCompletionRequirementEntity(ModuleCompletionRequirement(id = 1, minScore = 10.0), 2) + ModuleCompletionRequirementEntity(ModuleCompletionRequirement(id = 1, minScore = 10.0), 2, 2) ) } @Test fun testModuleItemCascade() = runTest { moduleCompletionRequirementDao.insert( - ModuleCompletionRequirementEntity(ModuleCompletionRequirement(id = 1, minScore = 10.0), 1) + ModuleCompletionRequirementEntity(ModuleCompletionRequirement(id = 1, minScore = 10.0), 1, 1) ) - moduleObjectDao.delete(ModuleObjectEntity(ModuleObject(id = 1), 1)) + courseDao.deleteByIds(listOf(1)) val result = moduleCompletionRequirementDao.findByModuleId(1) diff --git a/libs/pandautils/src/main/assets/discussion_topic_header_html_template.html b/libs/pandautils/src/main/assets/discussion_topic_header_html_template.html index 076ac0b643..b2b9e8c605 100644 --- a/libs/pandautils/src/main/assets/discussion_topic_header_html_template.html +++ b/libs/pandautils/src/main/assets/discussion_topic_header_html_template.html @@ -29,7 +29,7 @@