From 8ae953515be84c9fd6af2e2d841575290f2a602c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 14 May 2024 10:40:58 +0200 Subject: [PATCH 1/4] MOBILE-4470 modicon: Fix branding when icon changed --- src/core/components/mod-icon/mod-icon.ts | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/core/components/mod-icon/mod-icon.ts b/src/core/components/mod-icon/mod-icon.ts index 113cb6d54..4e883f201 100644 --- a/src/core/components/mod-icon/mod-icon.ts +++ b/src/core/components/mod-icon/mod-icon.ts @@ -51,7 +51,9 @@ export class CoreModIconComponent implements OnInit, OnChanges { @Input() showAlt = true; // Show alt otherwise it's only presentation icon. @Input() purpose: ModPurpose = ModPurpose.MOD_PURPOSE_OTHER; // Purpose of the module. @Input() @HostBinding('class.colorize') colorize = true; // Colorize the icon. Only applies on 4.0 onwards. - @Input() @HostBinding('class.branded') isBranded?: boolean; // If icon is branded and no colorize will be applied. + @Input() isBranded?: boolean; // If icon is branded and no colorize will be applied. + + @HostBinding('class.branded') brandedClass?: boolean; @HostBinding('attr.role') get getRole(): string | null { @@ -112,9 +114,9 @@ export class CoreModIconComponent implements OnInit, OnChanges { /** * Sets the isBranded property when undefined. */ - protected async setIsBranded(): Promise { + protected async setBrandedClass(): Promise { if (!this.colorize) { - this.isBranded = false; + this.brandedClass = false; // It doesn't matter. return; @@ -122,37 +124,36 @@ export class CoreModIconComponent implements OnInit, OnChanges { // Earlier 4.0, icons were never colorized. if (this.iconVersion === IconVersion.LEGACY_VERSION) { - this.isBranded = false; + this.brandedClass = false; this.colorize = false; return; } + // Reset the branded class to the original value. + this.brandedClass = this.isBranded; + // No icon or local icon (not legacy), colorize it. if (!this.iconUrl || this.isLocalUrl) { // Exception for bigbluebuttonbn, it's the only one that has a branded icon. if (this.iconVersion === IconVersion.VERSION_4_0 && this.modname === 'bigbluebuttonbn') { - this.isBranded = true; + this.brandedClass = true; return; } - this.isBranded ??= false; + this.brandedClass ??= false; return; } this.iconUrl = CoreTextUtils.decodeHTMLEntities(this.iconUrl); - if (this.isBranded !== undefined) { - return; - } - // If it's an Moodle Theme icon, check if filtericon is set and use it. if (this.iconUrl && CoreUrlUtils.isThemeImageUrl(this.iconUrl)) { const filter = CoreUrlUtils.getThemeImageUrlParam(this.iconUrl, 'filtericon'); if (filter === '1') { - this.isBranded = false; + this.brandedClass = false; return; } @@ -161,7 +162,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { if (this.modname && !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan(['4.0.8', '4.1.3', '4.2'])) { // If version is prior to that, check if the url is a module icon and filter it. if (this.getComponentNameFromIconUrl(this.iconUrl) === this.modname) { - this.isBranded = false; + this.brandedClass = false; return; } @@ -169,7 +170,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { } // External icons, or non monologo, do not filter. - this.isBranded = true; + this.brandedClass = true; } /** @@ -180,7 +181,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { if (!this.iconUrl) { this.loadFallbackIcon(); - this.setIsBranded(); + this.setBrandedClass(); return; } @@ -196,7 +197,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { !this.isLocalUrl && this.getComponentNameFromIconUrl(this.iconUrl) != this.modname; - this.setIsBranded(); + this.setBrandedClass(); await this.setSVGIcon(); } @@ -370,7 +371,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { // Has own styles, do not apply colors. if (doc.documentElement.getElementsByTagName('style').length > 0) { - this.isBranded = true; + this.brandedClass = true; } // Recursively remove attributes starting with on. From 58ef2f9e0e937617ca7d41b0d7bff76d345095ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 14 May 2024 10:41:16 +0200 Subject: [PATCH 2/4] MOBILE-4470 course: Add function to control if a module is on core --- src/core/components/mod-icon/mod-icon.ts | 2 +- src/core/features/course/services/course.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/core/components/mod-icon/mod-icon.ts b/src/core/components/mod-icon/mod-icon.ts index 4e883f201..e8a09cdd0 100644 --- a/src/core/components/mod-icon/mod-icon.ts +++ b/src/core/components/mod-icon/mod-icon.ts @@ -213,7 +213,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { this.isLocalUrl = true; this.linkIconWithComponent = false; - const moduleName = !this.modname || CoreCourse.CORE_MODULES.indexOf(this.modname) < 0 + const moduleName = !this.modname || !CoreCourse.isCoreModule(this.modname) ? fallbackModName : this.modname; diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 6542edd7f..011187191 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -137,7 +137,7 @@ export class CoreCourseProvider { static readonly COMPONENT = 'CoreCourse'; - readonly CORE_MODULES = [ + static readonly CORE_MODULES = [ 'assign', 'bigbluebuttonbn', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', 'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop', ]; @@ -848,7 +848,7 @@ export class CoreCourseProvider { return mimetypeIcon; } - if (this.CORE_MODULES.indexOf(moduleName) < 0) { + if (!CoreCourse.isCoreModule(moduleName)) { if (modicon) { return modicon; } @@ -1324,6 +1324,17 @@ export class CoreCourseProvider { return !!module.url; } + /** + * Check if the module is a core module. + * + * @param moduleName The module name. + * @returns Whether it's a core module. + */ + isCoreModule(moduleName: string): boolean { + // If core modules are removed for a certain version we should check the version of the site. + return CoreCourseProvider.CORE_MODULES.includes(moduleName); + } + /** * Wait for any course format plugin to load, and open the course page. * From f8e022c1fd8b7ec7f91a1177f6d5d2639b702ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 14 May 2024 11:22:46 +0200 Subject: [PATCH 3/4] MOBILE-4470 styles: Revisit text wrapping ellipsis --- .../recentlyaccesseditems.scss | 4 +--- .../timeline/components/events/events.scss | 3 +-- src/addons/messages/messages-common.scss | 4 +--- .../messages/pages/discussion/discussion.scss | 4 +--- .../pages/user-attempts/user-attempts.scss | 5 +++-- .../pages/users-attempts/users-attempts.scss | 5 +++-- src/addons/notifications/pages/list/list.scss | 6 +----- src/core/components/combobox/combobox.scss | 11 ++++------- src/core/components/message/message.scss | 4 +--- src/core/components/tabs/tabs.scss | 4 +--- .../course-format/course-format.html | 4 ++-- .../course-format/course-format.scss | 3 --- .../course-list-item/course-list-item.scss | 13 +++---------- .../global-search-result.scss | 11 +++-------- src/theme/components/format-text.scss | 4 +--- src/theme/components/ion-button.scss | 14 +++++++++++--- src/theme/components/ion-header.scss | 4 +--- src/theme/components/ion-item.scss | 4 +--- src/theme/helpers/custom.mixins.scss | 18 ++++++++++++++++++ 19 files changed, 57 insertions(+), 68 deletions(-) diff --git a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss index 9f161779f..1d64d9e8c 100644 --- a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss +++ b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss @@ -9,9 +9,7 @@ .ion-text-wrap ion-label { .item-heading, h2, p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + @include ellipsis(); } } } diff --git a/src/addons/block/timeline/components/events/events.scss b/src/addons/block/timeline/components/events/events.scss index 709f503bd..16fdec4a5 100644 --- a/src/addons/block/timeline/components/events/events.scss +++ b/src/addons/block/timeline/components/events/events.scss @@ -52,8 +52,7 @@ h4.core-bold { flex-wrap: wrap; & > span { - overflow: hidden; - text-overflow: ellipsis; + @include ellipsis(); } } diff --git a/src/addons/messages/messages-common.scss b/src/addons/messages/messages-common.scss index da75d7348..20a561db1 100644 --- a/src/addons/messages/messages-common.scss +++ b/src/addons/messages/messages-common.scss @@ -46,9 +46,7 @@ } .addon-message-last-message-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + @include ellipsis(); flex-shrink: 1; } } diff --git a/src/addons/messages/pages/discussion/discussion.scss b/src/addons/messages/pages/discussion/discussion.scss index 8a1e2dd70..b0936e504 100644 --- a/src/addons/messages/pages/discussion/discussion.scss +++ b/src/addons/messages/pages/discussion/discussion.scss @@ -51,9 +51,7 @@ } core-format-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + @include ellipsis(); flex-shrink: 1; display: block; } diff --git a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.scss b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.scss index eee2a95b4..0160b497f 100644 --- a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.scss +++ b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.scss @@ -1,10 +1,11 @@ +@use "theme/globals" as *; + :host { .addon-mod_h5pactivity-table-header { font-weight: bold; ion-col { - overflow: hidden; - text-overflow: ellipsis; + @include ellipsis(); } } diff --git a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.scss b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.scss index 4f70d0bdd..68eda35cb 100644 --- a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.scss +++ b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.scss @@ -1,6 +1,7 @@ +@use "theme/globals" as *; + :host { .addon-mod_h5pactivity-table-header ion-col { - overflow: hidden; - text-overflow: ellipsis; + @include ellipsis(); } } diff --git a/src/addons/notifications/pages/list/list.scss b/src/addons/notifications/pages/list/list.scss index 979f8c2d8..645c86029 100644 --- a/src/addons/notifications/pages/list/list.scss +++ b/src/addons/notifications/pages/list/list.scss @@ -7,11 +7,7 @@ ion-item.addon-notification-item { margin-bottom: 8px; p.item-heading { font-size: var(--text-size); - -webkit-line-clamp: 3; - overflow: hidden; - text-overflow: ellipsis; - -webkit-box-orient: vertical; - display: -webkit-box; + @include ellipsis(3); } p { font-size: 12px; diff --git a/src/core/components/combobox/combobox.scss b/src/core/components/combobox/combobox.scss index 320805a35..a42acf707 100644 --- a/src/core/components/combobox/combobox.scss +++ b/src/core/components/combobox/combobox.scss @@ -63,11 +63,11 @@ --padding-bottom: 8px; background: var(--background); + + --color: var(--core-combobox-color); color: var(--color); - text-overflow: ellipsis; - white-space: nowrap; + min-height: var(--a11y-sizing-minTargetSize); - overflow: hidden; box-shadow: var(--box-shadow); --highlight-color: transparent !important; @@ -121,7 +121,6 @@ } ion-button { - --color: var(--core-combobox-color); --color-activated: var(--core-combobox-color); --color-focused: currentcolor; --color-hover: currentcolor; @@ -130,9 +129,7 @@ .select-text { @include margin-horizontal(null, auto); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + @include ellipsis(); font: var(--mdl-typography-label-font-lg); } diff --git a/src/core/components/message/message.scss b/src/core/components/message/message.scss index f8395258c..ce49669aa 100644 --- a/src/core/components/message/message.scss +++ b/src/core/components/message/message.scss @@ -67,9 +67,7 @@ flex-grow: 1; padding-left: .5rem; padding-right: .5rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + @include ellipsis(); font-size: 16px; } } diff --git a/src/core/components/tabs/tabs.scss b/src/core/components/tabs/tabs.scss index 876bc93be..ca2cae2b5 100644 --- a/src/core/components/tabs/tabs.scss +++ b/src/core/components/tabs/tabs.scss @@ -61,9 +61,7 @@ max-width: 100%; ion-label { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; + @include ellipsis(); word-wrap: break-word; max-width: 100%; line-height: 1.2em; diff --git a/src/core/features/course/components/course-format/course-format.html b/src/core/features/course/components/course-format/course-format.html index 423cf7031..1f49076f2 100644 --- a/src/core/features/course/components/course-format/course-format.html +++ b/src/core/features/course/components/course-format/course-format.html @@ -31,12 +31,12 @@
+ [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name" class="ion-text-nowrap"> + [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name" class="ion-text-nowrap"> diff --git a/src/core/features/course/components/course-format/course-format.scss b/src/core/features/course/components/course-format/course-format.scss index 8fe3690a7..6b7420c86 100644 --- a/src/core/features/course/components/course-format/course-format.scss +++ b/src/core/features/course/components/course-format/course-format.scss @@ -5,9 +5,6 @@ padding-right: 8px; ion-button { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; flex: 1; margin-left: 4px; margin-right: 4px; diff --git a/src/core/features/courses/components/course-list-item/course-list-item.scss b/src/core/features/courses/components/course-list-item/course-list-item.scss index 05d8c8b74..860f74121 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.scss +++ b/src/core/features/courses/components/course-list-item/course-list-item.scss @@ -87,9 +87,7 @@ ion-card { .core-course-shortname { font-size: var(--shortname-size); color: var(--dark); - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; + @include ellipsis(); } ion-chip { @@ -226,18 +224,13 @@ ion-card.core-course-list-card { // Clamp one line with ellipsis on tablet view, and 2 in mobile. .item-heading { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; + @include ellipsis(); } @include media-breakpoint-down(md) { .item-heading { // Addition lines for 2 line or multiline ellipsis - display: -webkit-box !important; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - white-space: normal; + @include ellipsis(2); } } diff --git a/src/core/features/search/components/global-search-result/global-search-result.scss b/src/core/features/search/components/global-search-result/global-search-result.scss index bc5ac4ee8..b4fa7d514 100644 --- a/src/core/features/search/components/global-search-result/global-search-result.scss +++ b/src/core/features/search/components/global-search-result/global-search-result.scss @@ -1,3 +1,5 @@ +@use "theme/globals" as *; + :host ion-item { --core-global-search-result-image-size: 40px; --core-global-search-result-title-color: var(--text); @@ -52,14 +54,7 @@ core-format-text { color: var(--core-global-search-result-content-color); - @supports (-webkit-line-clamp: 2) { - white-space: normal; - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - } - + @include ellipsis(2); } .result-context-wrapper { diff --git a/src/theme/components/format-text.scss b/src/theme/components/format-text.scss index d33aa6793..d7e304b63 100644 --- a/src/theme/components/format-text.scss +++ b/src/theme/components/format-text.scss @@ -620,9 +620,7 @@ core-rich-text-editor .core-rte-editor { .text-wrap { white-space: normal !important; } .text-nowrap { white-space: nowrap !important; } .text-truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + @include ellipsis(); } .text-left { text-align: left !important; } .text-right { text-align: right !important; } diff --git a/src/theme/components/ion-button.scss b/src/theme/components/ion-button.scss index 3588db15b..5fd9e9628 100644 --- a/src/theme/components/ion-button.scss +++ b/src/theme/components/ion-button.scss @@ -5,13 +5,21 @@ ion-button { line-height: 120%; core-format-text { - white-space: normal; display: contents; line-height: 120%; } - & > * { - white-space: normal; + &.ion-text-nowrap { + @include ellipsis(); + + & > * { + @include ellipsis(); + + } + + core-format-text { + display: block; + } } ion-spinner { diff --git a/src/theme/components/ion-header.scss b/src/theme/components/ion-header.scss index e006f8e80..597306033 100644 --- a/src/theme/components/ion-header.scss +++ b/src/theme/components/ion-header.scss @@ -61,9 +61,7 @@ ion-header.header-md { @include padding(0, 16px); h1, h2, .subheading { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; + @include ellipsis(); margin: 0; } diff --git a/src/theme/components/ion-item.scss b/src/theme/components/ion-item.scss index 86eb2ec81..18ce5a72a 100644 --- a/src/theme/components/ion-item.scss +++ b/src/theme/components/ion-item.scss @@ -193,9 +193,7 @@ ion-toggle::part(label), ion-input > label { core-format-text, core-format-text > *:not(pre) { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + @include ellipsis(); } } diff --git a/src/theme/helpers/custom.mixins.scss b/src/theme/helpers/custom.mixins.scss index 22272230f..b3e49d758 100644 --- a/src/theme/helpers/custom.mixins.scss +++ b/src/theme/helpers/custom.mixins.scss @@ -153,6 +153,24 @@ border: 0; } +@mixin ellipsis($lines: 1) { + @if ($lines == 1) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } @else { + // Only supported on Android 124+, iOs 11+. https://caniuse.com/css-line-clamp + @supports (-webkit-line-clamp: 2) { + -webkit-line-clamp: $lines; + -webkit-box-orient: vertical; + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + } + } +} + /** * Same as item-push-svg-url but admits flip-rtl */ From 8f461adf74661bf9eba1d7304c4b4c04e2d77cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 14 May 2024 14:55:06 +0200 Subject: [PATCH 4/4] MOBILE-4470 choice: Fix messages shown on the app --- scripts/langindex.json | 4 +- src/addons/mod/choice/choice.module.ts | 4 +- .../index/addon-mod-choice-index.html | 49 +++++----- .../mod/choice/components/index/index.ts | 90 ++++++++++--------- src/addons/mod/choice/constants.ts | 29 ++++++ src/addons/mod/choice/lang.json | 4 +- src/addons/mod/choice/services/choice-sync.ts | 5 +- src/addons/mod/choice/services/choice.ts | 70 +++++++++------ .../mod/choice/services/handlers/prefetch.ts | 7 +- 9 files changed, 158 insertions(+), 104 deletions(-) create mode 100644 src/addons/mod/choice/constants.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index fa870638e..08019c959 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -524,15 +524,13 @@ "addon.mod_choice.cannotsubmit": "choice", "addon.mod_choice.choiceoptions": "choice", "addon.mod_choice.errorgetchoice": "local_moodlemobileapp", - "addon.mod_choice.expired": "choice", "addon.mod_choice.full": "choice", "addon.mod_choice.limita": "choice", "addon.mod_choice.modulenameplural": "choice", "addon.mod_choice.noresultsviewable": "choice", - "addon.mod_choice.notopenyet": "choice", "addon.mod_choice.numberofuser": "choice", "addon.mod_choice.numberofuserinpercentage": "choice", - "addon.mod_choice.previewonly": "choice", + "addon.mod_choice.previewing": "choice", "addon.mod_choice.publishinfoanonafter": "choice", "addon.mod_choice.publishinfoanonclose": "choice", "addon.mod_choice.publishinfofullafter": "choice", diff --git a/src/addons/mod/choice/choice.module.ts b/src/addons/mod/choice/choice.module.ts index dbf056f0f..b09b8ab97 100644 --- a/src/addons/mod/choice/choice.module.ts +++ b/src/addons/mod/choice/choice.module.ts @@ -22,13 +22,13 @@ import { CoreCourseModulePrefetchDelegate } from '@features/course/services/modu import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreCronDelegate } from '@services/cron'; import { CORE_SITE_SCHEMAS } from '@services/sites'; -import { AddonModChoiceProvider } from './services/choice'; import { OFFLINE_SITE_SCHEMA } from './services/database/choice'; import { AddonModChoiceIndexLinkHandler } from './services/handlers/index-link'; import { AddonModChoiceListLinkHandler } from './services/handlers/list-link'; import { AddonModChoiceModuleHandler, AddonModChoiceModuleHandlerService } from './services/handlers/module'; import { AddonModChoicePrefetchHandler } from './services/handlers/prefetch'; import { AddonModChoiceSyncCronHandler } from './services/handlers/sync-cron'; +import { ADDON_MOD_CHOICE_COMPONENT } from './constants'; const routes: Routes = [ { @@ -57,7 +57,7 @@ const routes: Routes = [ CoreContentLinksDelegate.registerHandler(AddonModChoiceIndexLinkHandler.instance); CoreContentLinksDelegate.registerHandler(AddonModChoiceListLinkHandler.instance); - CoreCourseHelper.registerModuleReminderClick(AddonModChoiceProvider.COMPONENT); + CoreCourseHelper.registerModuleReminderClick(ADDON_MOD_CHOICE_COMPONENT); }, }, ], diff --git a/src/addons/mod/choice/components/index/addon-mod-choice-index.html b/src/addons/mod/choice/components/index/addon-mod-choice-index.html index 80374ed11..fd8aa8e1d 100644 --- a/src/addons/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addons/mod/choice/components/index/addon-mod-choice-index.html @@ -12,32 +12,17 @@ [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()" /> - + - - - - - - + - + - + - + + + + + +

{{ 'addon.mod_choice.yourselection' | translate }}

+
+
+ + + + + + + +
+ -
+

{{ 'addon.mod_choice.responses' | translate }}

@@ -115,9 +116,9 @@ -
+ - +