Merge pull request #3156 from crazyserver/MOBILE-3814

Mobile 3814
main
Dani Palou 2022-03-07 07:42:40 +01:00 committed by GitHub
commit 8ece3c722b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 287 additions and 146 deletions

View File

@ -1106,6 +1106,7 @@
"addon.storagemanager.confirmdeletedatafrom": "local_moodlemobileapp", "addon.storagemanager.confirmdeletedatafrom": "local_moodlemobileapp",
"addon.storagemanager.coursedownloads": "local_moodlemobileapp", "addon.storagemanager.coursedownloads": "local_moodlemobileapp",
"addon.storagemanager.courseinfo": "local_moodlemobileapp", "addon.storagemanager.courseinfo": "local_moodlemobileapp",
"addon.storagemanager.deleteall": "moodle",
"addon.storagemanager.deleteallsitedata": "local_moodlemobileapp", "addon.storagemanager.deleteallsitedata": "local_moodlemobileapp",
"addon.storagemanager.deleteallsitedatainfo": "local_moodlemobileapp", "addon.storagemanager.deleteallsitedatainfo": "local_moodlemobileapp",
"addon.storagemanager.deletecourses": "local_moodlemobileapp", "addon.storagemanager.deletecourses": "local_moodlemobileapp",

View File

@ -4,7 +4,7 @@
</ion-label> </ion-label>
<div slot="end" class="flex-row"> <div slot="end" class="flex-row">
<!-- Download all courses. --> <!-- Download all courses. -->
<div *ngIf="downloadCoursesEnabled && filteredCourses.length > 1" class="core-button-spinner"> <div *ngIf="downloadCoursesEnabled && filteredCourses.length > 0" class="core-button-spinner">
<ion-button *ngIf="!prefetchCoursesData.loading" fill="clear" (click)="prefetchCourses()" <ion-button *ngIf="!prefetchCoursesData.loading" fill="clear" (click)="prefetchCourses()"
[attr.aria-label]="prefetchCoursesData.statusTranslatable | translate"> [attr.aria-label]="prefetchCoursesData.statusTranslatable | translate">
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"> <ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true">

View File

@ -5,6 +5,7 @@
"confirmdeletedatafrom": "Delete all downloaded data from '{{name}}'?", "confirmdeletedatafrom": "Delete all downloaded data from '{{name}}'?",
"coursedownloads": "Course downloads", "coursedownloads": "Course downloads",
"courseinfo": "Download course content to work offline. Your activity will sync automatically when your device is back online.", "courseinfo": "Download course content to work offline. Your activity will sync automatically when your device is back online.",
"deleteall": "Delete all",
"deleteallsitedata": "Delete all site downloaded data", "deleteallsitedata": "Delete all site downloaded data",
"deleteallsitedatainfo": "This will delete all downloaded data from '{{name}}', including all downloaded courses and data that allows you to work offline.", "deleteallsitedatainfo": "This will delete all downloaded data from '{{name}}', including all downloaded courses and data that allows you to work offline.",
"deletecourses": "Delete downloaded data from all courses", "deletecourses": "Delete downloaded data from all courses",

View File

@ -10,36 +10,51 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<core-loading [hideUntil]="loaded" class="list-item-limited-width"> <core-loading [hideUntil]="loaded" class="list-item-limited-width">
<ion-item class="ion-text-wrap">
<ion-label>
<p>{{ 'addon.storagemanager.courseinfo' | translate }}</p>
</ion-label>
</ion-item>
<ion-card class="wholecourse"> <ion-card class="wholecourse">
<ion-card-header> <ion-card-header>
<p class="ion-text-wrap ion-no-margin">{{ 'addon.storagemanager.courseinfo' | translate }}</p>
<ion-card-title>{{ title }}</ion-card-title> <ion-card-title>{{ title }}</ion-card-title>
<ion-item class="size ion-text-wrap ion-no-padding"> <ion-item class="size ion-text-wrap ion-no-padding">
<ion-label> <ion-label>
<p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totaldownloads' | translate }}</p> <p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totaldownloads' | translate }}</p>
<ion-badge color="light">{{ totalSize | coreBytesToSize }}</ion-badge>
</ion-label> </ion-label>
<ion-button slot="end" (click)="deleteForCourse()" [disabled]="totalSize == 0" color="danger" fill="clear"> <ion-badge color="light" slot="end">{{ totalSize | coreBytesToSize }}
<ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: </ion-badge>
{ name: title }">
</ion-icon>
</ion-button>
</ion-item> </ion-item>
<ion-button *ngIf="downloadCourseEnabled" (click)="prefetchCourse()" expand="block" fill="outline"> <ion-button *ngIf="downloadCourseEnabled" (click)="prefetchCourse()" expand="block" fill="outline" class="ion-no-margin">
<ion-icon *ngIf="!prefetchCourseData.loading" [name]="prefetchCourseData.icon" slot="start"></ion-icon> <ion-icon *ngIf="!prefetchCourseData.loading" [name]="prefetchCourseData.icon" slot="start"></ion-icon>
<ion-spinner *ngIf="prefetchCourseData.loading" slot="start"></ion-spinner> <ion-spinner *ngIf="prefetchCourseData.loading" slot="start"></ion-spinner>
{{ prefetchCourseData.statusTranslatable | translate }} {{ prefetchCourseData.statusTranslatable | translate }}
</ion-button> </ion-button>
<ion-button *ngIf="totalSize > 0" (click)="deleteForCourse()" expand="block" color="danger"
class="ion-no-margin ion-margin-top">
<ion-icon name="fas-trash" slot="start" [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate:
{ name: title }">
</ion-icon>
{{ 'addon.storagemanager.deleteall' | translate }}
</ion-button>
</ion-card-header> </ion-card-header>
</ion-card> </ion-card>
<ng-container *ngFor="let section of sections"> <ng-container *ngFor="let section of sections">
<ion-card class="section" *ngIf="section.modules.length > 0"> <ion-card class="section" *ngIf="section.modules.length > 0">
<ion-card-header> <ion-card-header>
<ion-item class="ion-no-padding"> <ion-item class="ion-no-padding" lines="full">
<ion-label> <ion-label>
<p class="item-heading ion-text-wrap">{{ section.name }}</p> <p class="item-heading ion-text-wrap">
<ion-badge color="light" *ngIf="section.totalSize > 0"> <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
{{ section.totalSize | coreBytesToSize }} [adaptImg]="false">
</core-format-text>
</p>
<ion-badge [color]="section.downloadStatus == statusDownloaded ? 'success' : 'light'"
*ngIf="section.totalSize > 0">
<ion-icon aria-hidden="true" name="fam-cloud-done" *ngIf="section.downloadStatus == statusDownloaded">
</ion-icon>{{ section.totalSize | coreBytesToSize }}
</ion-badge> </ion-badge>
<!-- Download progress. --> <!-- Download progress. -->
<p *ngIf="downloadEnabled && section.isDownloading"> <p *ngIf="downloadEnabled && section.isDownloading">
@ -48,15 +63,10 @@
</p> </p>
</ion-label> </ion-label>
<div class="storage-buttons" slot="end" *ngIf="section.totalSize > 0 || downloadEnabled"> <div class="storage-buttons" slot="end" *ngIf="section.totalSize > 0 || downloadEnabled">
<ion-button (click)="deleteForSection(section)" *ngIf="section.totalSize > 0" color="danger" fill="clear">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }">
</ion-icon>
</ion-button>
<div *ngIf="downloadEnabled" slot="end" class="core-button-spinner"> <div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
<core-download-refresh *ngIf="!section.isDownloading" [status]="section.downloadStatus" [enabled]="true" <core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus != statusDownloaded"
(action)="prefecthSection(section)" [loading]="section.isDownloading || section.isCalculating" [status]="section.downloadStatus" [enabled]="true" (action)="prefecthSection(section)"
[canTrustDownload]="true"> [loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="true">
</core-download-refresh> </core-download-refresh>
<ion-badge class="core-course-download-section-progress" <ion-badge class="core-course-download-section-progress"
@ -66,6 +76,11 @@
{{section.count}} / {{section.total}} {{section.count}} / {{section.total}}
</ion-badge> </ion-badge>
</div> </div>
<ion-button (click)="deleteForSection(section)" *ngIf="section.totalSize > 0" color="danger" fill="clear">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }">
</ion-icon>
</ion-button>
</div> </div>
</ion-item> </ion-item>
</ion-card-header> </ion-card-header>
@ -81,22 +96,25 @@
[contextInstanceId]="module.id" [adaptImg]="false"> [contextInstanceId]="module.id" [adaptImg]="false">
</core-format-text> </core-format-text>
</h3> </h3>
<ion-badge color="light" *ngIf="module.totalSize > 0"> <ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
{{ module.totalSize | coreBytesToSize }} *ngIf="module.totalSize > 0">
<ion-icon aria-hidden="true" name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded">
</ion-icon>{{ module.totalSize | coreBytesToSize }}
</ion-badge> </ion-badge>
</ion-label> </ion-label>
<div class="storage-buttons" slot="end"> <div class="storage-buttons" slot="end">
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
(action)="prefetchModule(module, section)">
</core-download-refresh>
<ion-button fill="clear" (click)="deleteForModule(module, section)" *ngIf="module.totalSize > 0" <ion-button fill="clear" (click)="deleteForModule(module, section)" *ngIf="module.totalSize > 0"
color="danger"> color="danger">
<ion-icon name="fas-trash" slot="icon-only" <ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }"> [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
</ion-icon> </ion-icon>
</ion-button> </ion-button>
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton"
[status]="module.downloadStatus" [enabled]="true" [canTrustDownload]="true"
[loading]="module.spinner || module.handlerData.spinner" (action)="prefetchModule(module, section)">
</core-download-refresh>
</div> </div>
</ion-item> </ion-item>
</ng-container> </ng-container>

View File

@ -30,6 +30,20 @@
} }
} }
ion-badge {
margin-top: 8px;
ion-icon {
@include margin-horizontal(null, 8px);
}
}
ion-item core-mod-icon {
--size: 18px;
padding: 9px;
--margin-vertical: 8px;
--margin-end: 8px;
}
.storage-buttons { .storage-buttons {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -59,6 +59,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
loading: true, loading: true,
}; };
statusDownloaded = CoreConstants.DOWNLOADED;
protected siteUpdatedObserver?: CoreEventObserver; protected siteUpdatedObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver; protected courseStatusObserver?: CoreEventObserver;
protected sectionStatusObserver?: CoreEventObserver; protected sectionStatusObserver?: CoreEventObserver;

View File

@ -1,4 +1,10 @@
:host { :host {
--textarea-background: var(--core-input-background);
--textarea-color: var(--core-input-text);
--textarea-border-width: var(--core-input-border-width);
--textarea-border-color: var(--core-input-stroke);
--textarea-radius: var(--huge-radius);
form { form {
position: relative; position: relative;
display: flex; display: flex;
@ -11,14 +17,17 @@
appearance: none; appearance: none;
display: block; display: block;
width: 100%; width: 100%;
border: 0;
font-family: inherit; font-family: inherit;
background: var(--core-send-message-input-background);
color: var(--core-send-message-input-color); background: var(--textarea-background);
border-radius: 21px; color: var(--textarea-color);
border: var(--textarea-border-width) solid var(--textarea-border-color);
border-radius: var(--textarea-radius);
min-height: var(--a11y-min-target-size);
line-height: 20px; line-height: 20px;
padding: 9px 12px 11px; padding: 10px;
margin: 5px 10px; margin: 4px 8px;
resize: vertical; resize: vertical;
} }

View File

@ -39,7 +39,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
protected initialPaddingBottom = '0px'; protected initialPaddingBottom = '0px';
protected previousTop = 0; protected previousTop = 0;
protected previousHeight = 0; protected previousHeight = 0;
protected stickTimeout?: number; protected endAnimationTimeout?: number;
protected content?: HTMLIonContentElement | null; protected content?: HTMLIonContentElement | null;
protected loadingChangedListener?: CoreEventObserver; protected loadingChangedListener?: CoreEventObserver;
@ -60,6 +60,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
this.initialHeight = this.element.getBoundingClientRect().height || 48; this.initialHeight = this.element.getBoundingClientRect().height || 48;
this.previousHeight = this.initialHeight; this.previousHeight = this.initialHeight;
this.content?.style.setProperty('--core-collapsible-footer-max-height', this.initialHeight + 'px');
this.setBarHeight(this.initialHeight); this.setBarHeight(this.initialHeight);
} }
@ -88,7 +90,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
this.initialPaddingBottom = this.content.style.getPropertyValue('--padding-bottom') || this.initialPaddingBottom; this.initialPaddingBottom = this.content.style.getPropertyValue('--padding-bottom') || this.initialPaddingBottom;
this.content.style.setProperty( this.content.style.setProperty(
'--padding-bottom', '--padding-bottom',
`calc(${this.initialPaddingBottom} + var(--core-collapsible-footer-height, 0px))`, `calc(${this.initialPaddingBottom} + var(--core-collapsible-footer-max-height, 0px))`,
); );
const scroll = await this.content.getScrollElement(); const scroll = await this.content.getScrollElement();
@ -143,8 +145,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
* @param height The new bar height. * @param height The new bar height.
*/ */
protected setBarHeight(height: number): void { protected setBarHeight(height: number): void {
if (this.stickTimeout) { if (this.endAnimationTimeout) {
clearTimeout(this.stickTimeout); clearTimeout(this.endAnimationTimeout);
} }
this.element.classList.toggle('footer-collapsed', height <= 0); this.element.classList.toggle('footer-collapsed', height <= 0);
@ -154,12 +156,21 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
if (height > 0 && height < this.initialHeight) { if (height > 0 && height < this.initialHeight) {
// Finish opening or closing the bar. // Finish opening or closing the bar.
const newHeight = height < this.initialHeight / 2 ? 0 : this.initialHeight; this.endAnimationTimeout = window.setTimeout(() => this.endAnimation(height), 500);
this.stickTimeout = window.setTimeout(() => this.setBarHeight(newHeight), 500);
} }
} }
/**
* End of animation when not scrolling.
*
* @param height Last height used.
*/
protected endAnimation(height: number): void {
const newHeight = height < this.initialHeight / 2 ? 0 : this.initialHeight;
this.setBarHeight(newHeight);
}
/** /**
* @inheritdoc * @inheritdoc
*/ */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChange } from '@angular/core';
import { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet'; import { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet';
import { ScrollDetail } from '@ionic/core'; import { ScrollDetail } from '@ionic/core';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
@ -50,7 +50,9 @@ import { CoreFormatTextDirective } from './format-text';
@Directive({ @Directive({
selector: 'ion-header[collapsible]', selector: 'ion-header[collapsible]',
}) })
export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy { export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDestroy {
@Input() collapsible = true;
protected page?: HTMLElement; protected page?: HTMLElement;
protected collapsedHeader?: Element; protected collapsedHeader?: Element;
@ -63,6 +65,8 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy {
protected floatingTitle?: HTMLElement; protected floatingTitle?: HTMLElement;
protected scrollingHeight?: number; protected scrollingHeight?: number;
protected subscriptions: Subscription[] = []; protected subscriptions: Subscription[] = [];
protected enabled = true;
protected endAnimationTimeout?: number;
constructor(protected el: ElementRef) {} constructor(protected el: ElementRef) {}
@ -82,6 +86,18 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy {
this.initializeContent(); this.initializeContent();
} }
/**
* @inheritdoc
*/
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
if (changes.collapsible) {
this.enabled = !CoreUtils.isFalseOrZero(changes.collapsible.currentValue);
setTimeout(() => {
this.setEnabled(this.enabled);
}, 200);
}
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
@ -159,9 +175,6 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy {
}; };
this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated)); this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated));
this.subscriptions.push(outlet.deactivateEvents.subscribe(onOutletUpdated));
onOutletUpdated();
return; return;
} }
@ -277,6 +290,29 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy {
content && this.trackContentScroll(content); content && this.trackContentScroll(content);
} }
/**
* Set collapsed/expanded based on properties.
*
* @param enable True to enable, false otherwise
*/
async setEnabled(enable: boolean): Promise<void> {
if (!this.page || !this.content) {
return;
}
if (enable) {
const contentScroll = await this.content.getScrollElement();
// Do nothing, since scroll has already started on the page.
if (contentScroll.scrollTop > 0) {
return;
}
}
this.page.style.setProperty('--collapsible-header-progress', enable ? '0' : '1');
this.page.classList.toggle('is-collapsed', !enable);
}
/** /**
* Listen to a content element for scroll events that will control the header state transition. * Listen to a content element for scroll events that will control the header state transition.
* *
@ -287,6 +323,8 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy {
return; return;
} }
this.content = content;
const page = this.page; const page = this.page;
const scrollingHeight = this.scrollingHeight; const scrollingHeight = this.scrollingHeight;
const expandedHeader = this.expandedHeader; const expandedHeader = this.expandedHeader;
@ -294,7 +332,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy {
const expandedFontStyles = this.expandedFontStyles; const expandedFontStyles = this.expandedFontStyles;
const collapsedFontStyles = this.collapsedFontStyles; const collapsedFontStyles = this.collapsedFontStyles;
const floatingTitle = this.floatingTitle; const floatingTitle = this.floatingTitle;
const contentScroll = await content.getScrollElement(); const contentScroll = await this.content.getScrollElement();
if ( if (
!page || !page ||
@ -308,20 +346,22 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy {
throw new Error('[collapsible-header] Couldn\'t set up scrolling'); throw new Error('[collapsible-header] Couldn\'t set up scrolling');
} }
page.style.setProperty('--collapsible-header-progress', '0');
page.classList.toggle('is-within-content', content.contains(expandedHeader)); page.classList.toggle('is-within-content', content.contains(expandedHeader));
page.classList.toggle('is-collapsed', false); this.setEnabled(this.enabled);
Object Object
.entries(expandedFontStyles) .entries(expandedFontStyles)
.forEach(([property, value]) => floatingTitle.style.setProperty(property, value as string)); .forEach(([property, value]) => floatingTitle.style.setProperty(property, value as string));
this.content = content;
this.content.addEventListener('ionScroll', this.contentScrollListener = ({ target }: CustomEvent<ScrollDetail>): void => { this.content.addEventListener('ionScroll', this.contentScrollListener = ({ target }: CustomEvent<ScrollDetail>): void => {
if (target !== this.content) { if (target !== this.content || !this.enabled) {
return; return;
} }
if (this.endAnimationTimeout) {
clearTimeout(this.endAnimationTimeout);
}
const scrollableHeight = contentScroll.scrollHeight - contentScroll.clientHeight; const scrollableHeight = contentScroll.scrollHeight - contentScroll.clientHeight;
const collapsedHeight = expandedHeaderHeight - (expandedHeader.clientHeight ?? 0); const collapsedHeight = expandedHeaderHeight - (expandedHeader.clientHeight ?? 0);
const frozen = scrollableHeight + collapsedHeight <= 2 * expandedHeaderHeight; const frozen = scrollableHeight + collapsedHeight <= 2 * expandedHeaderHeight;
@ -336,7 +376,28 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnDestroy {
Object Object
.entries(progress > .5 ? collapsedFontStyles : expandedFontStyles) .entries(progress > .5 ? collapsedFontStyles : expandedFontStyles)
.forEach(([property, value]) => floatingTitle.style.setProperty(property, value as string)); .forEach(([property, value]) => floatingTitle.style.setProperty(property, value as string));
if (progress > 0 || progress < 1) {
// Finish opening or closing the bar.
this.endAnimationTimeout = window.setTimeout(() => this.endAnimation(progress), 500);
}
}); });
} }
/**
* End of animation when stop scrolling.
*
* @param progress Progress.
*/
protected endAnimation(progress: number): void {
if(!this.page) {
return;
}
const collapse = progress > 0.5;
this.page.style.setProperty('--collapsible-header-progress', collapse ? '1' : '0');
this.page.classList.toggle('is-collapsed', collapse);
}
} }

View File

@ -56,7 +56,9 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
el: this.element, el: this.element,
gestureName: 'swipe', gestureName: 'swipe',
threshold: 10, threshold: 10,
direction: 'x',
gesturePriority: 10, gesturePriority: 10,
maxAngle: 20,
canStart: () => this.enabled, canStart: () => this.enabled,
onStart: () => { onStart: () => {
style.transition = ''; style.transition = '';

View File

@ -12,6 +12,7 @@
--padding-end: 0; --padding-end: 0;
--border-radius: 2em 0 0 2em; --border-radius: 2em 0 0 2em;
height: 56px; height: 56px;
--box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12);
&::part(native) { &::part(native) {
@include core-transition(padding, 200ms); @include core-transition(padding, 200ms);

View File

@ -24,21 +24,21 @@
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading> <core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
</div> </div>
<div collapsible-footer *ngIf="displayCourseIndex && (previousSection || nextSection)" slot="fixed">
<div class="ion-padding core-course-section-nav-buttons safe-area-padding-horizontal list-item-limited-width" <div class="core-course-section-nav-buttons safe-area-padding-horizontal list-item-limited-width">
*ngIf="displayCourseIndex && (previousSection || nextSection)"> <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" expand="block"
<ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" expand="block" [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">
[attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> <ion-icon name="fas-arrow-left" slot="start" aria-hidden="true"></ion-icon>
<ion-icon name="fas-arrow-left" slot="start" aria-hidden="true"></ion-icon> <core-format-text [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course.id">
<core-format-text [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course.id"> </core-format-text>
</core-format-text> </ion-button>
</ion-button> <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" expand="block"
<ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" expand="block" [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name">
[attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name"> <core-format-text [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course.id">
<core-format-text [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course.id"> </core-format-text>
</core-format-text> <ion-icon name="fas-arrow-right" slot="end" aria-hidden="true"></ion-icon>
<ion-icon name="fas-arrow-right" slot="end" aria-hidden="true"></ion-icon> </ion-button>
</ion-button> </div>
</div> </div>
</core-loading> </core-loading>
</core-dynamic-component> </core-dynamic-component>

View File

@ -24,8 +24,8 @@
</ion-item> </ion-item>
<ng-container *ngIf="allSectionId != section.id"> <ng-container *ngIf="allSectionId != section.id">
<ion-item class="divider section" (click)="selectSectionOrModule($event, section.id)" button <ion-item class="divider section" (click)="selectSectionOrModule($event, section.id)" button
[class.item-current]="selectedId === section.id" [class.item-dimmed]="section.visible === 0" detail="false" [class.item-current]="selectedId === section.id" [class.item-dimmed]="section.visible === 0"
sticky="true"> [class.item-hightlighted]="section.highlighted" detail="false" sticky="true">
<ion-icon *ngIf="section.hasVisibleModules" name="fas-chevron-right" flip-rtl slot="start" <ion-icon *ngIf="section.hasVisibleModules" name="fas-chevron-right" flip-rtl slot="start"
class="expandable-status-icon" (click)="toggleExpand($event, section)" class="expandable-status-icon" (click)="toggleExpand($event, section)"
[attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate" [attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate"
@ -46,7 +46,7 @@
</ion-item> </ion-item>
<ng-container *ngIf="section.expanded"> <ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules"> <ng-container *ngFor="let module of section.modules">
<ion-item class="module" [class.item-dimmed]="!module.visible" <ion-item class="module" [class.item-dimmed]="!module.visible" [class.item-hightlighted]="section.highlighted"
(click)="selectSectionOrModule($event, section.id, module.id)" button> (click)="selectSectionOrModule($event, section.id, module.id)" button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined" <ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
slot="start" aria-hidden="true"></ion-icon> slot="start" aria-hidden="true"></ion-icon>

View File

@ -11,56 +11,68 @@ core-progress-bar {
} }
} }
ion-icon.completioninfo { ion-item.item {
font-size: 10px; &.core-course-index-all::part(native) {
width: 18px; --padding-start: 16px;
} }
ion-item.module::part(native) {
--padding-start: 0;
}
ion-item.module ion-icon {
margin: 0;
@include padding(12px, 16px, 12px, 16px);
}
ion-item.core-course-index-all::part(native) {
--padding-start: 16px;
}
ion-item.item.item-current {
--background: var(--primary-tint);
border: 0;
}
ion-icon.restricted {
font-size: 14px;
}
ion-item.item.divider.section {
--padding-start: 0px;
&.item-current { &.item-current {
ion-badge { --background: var(--primary-tint);
border: 1px solid var(--primary-contrast); border: 0;
}
&.item-hightlighted {
@include safe-area-border-start(var(--selected-item-border-width), solid, var(--selected-item-color));
}
&.divider.section {
--padding-start: 0px;
&.item-current {
ion-icon.expandable-status-icon {
color: var(--primary-contrast);
&:hover {
background: var(--primary-shade);
}
}
} }
ion-icon.expandable-status-icon { ion-icon.expandable-status-icon {
color: var(--primary-contrast); padding: 13px;
margin: 3px;
border-radius: 50%;
&:hover { &:hover {
background: var(--primary-shade); background: var(--gray-300);
} }
} }
&.item-hightlighted ion-icon.expandable-status-icon {
@include margin-horizontal(-2px, null);
}
} }
ion-icon.expandable-status-icon { &.module {
padding: 13px; &::part(native) {
margin: 3px; --padding-start: 0;
border-radius: 50%; }
&:hover {
background: var(--gray-300); &.item-hightlighted ion-icon.completioninfo {
@include padding-horizontal(11px, null);
}
}
ion-icon {
margin: 0;
padding: 12px 16px;
&.completioninfo {
font-size: 10px;
width: 18px;
}
&.restricted {
font-size: 14px;
} }
} }
} }

View File

@ -1,4 +1,4 @@
<ion-header collapsible> <ion-header [collapsible]="tabsComponent?.selectedIndex == 0 || tabsComponent?.selectedIndex === undefined">
<ion-toolbar> <ion-toolbar>
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button> <ion-back-button [text]="'core.back' | translate"></ion-back-button>

View File

@ -27,7 +27,6 @@ import { CoreTextUtils } from '@services/utils/text';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CONTENTS_PAGE_NAME } from '@features/course/course.module'; import { CONTENTS_PAGE_NAME } from '@features/course/course.module';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreCollapsibleHeaderDirective } from '@directives/collapsible-header';
import { CoreCourseSummaryPage } from '../course-summary/course-summary'; import { CoreCourseSummaryPage } from '../course-summary/course-summary';
/** /**
@ -41,7 +40,6 @@ import { CoreCourseSummaryPage } from '../course-summary/course-summary';
export class CoreCourseIndexPage implements OnInit, OnDestroy { export class CoreCourseIndexPage implements OnInit, OnDestroy {
@ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent; @ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent;
@ViewChild(CoreCollapsibleHeaderDirective) ionCollapsibleHeader?: CoreCollapsibleHeaderDirective;
title = ''; title = '';
category = ''; category = '';

View File

@ -12,7 +12,7 @@
"completeenrolmentbrowser": "Complete enrolment in browser", "completeenrolmentbrowser": "Complete enrolment in browser",
"confirmselfenrol": "Are you sure you want to enrol yourself in this course?", "confirmselfenrol": "Are you sure you want to enrol yourself in this course?",
"courses": "Courses", "courses": "Courses",
"downloadcourses": "Download courses", "downloadcourses": "Download all courses",
"enrolme": "Enrol me", "enrolme": "Enrol me",
"errorloadcategories": "An error occurred while loading categories.", "errorloadcategories": "An error occurred while loading categories.",
"errorloadcourses": "An error occurred while loading courses.", "errorloadcourses": "An error occurred while loading courses.",

View File

@ -29,7 +29,7 @@
</ion-label> </ion-label>
<div slot="end" class="flex-row"> <div slot="end" class="flex-row">
<!-- Download all courses. --> <!-- Download all courses. -->
<div *ngIf="downloadCoursesEnabled && myOverviewBlock && myOverviewBlock.filteredCourses.length > 1" <div *ngIf="downloadCoursesEnabled && myOverviewBlock && myOverviewBlock.filteredCourses.length > 0"
class="core-button-spinner"> class="core-button-spinner">
<ion-button *ngIf="!myOverviewBlock.prefetchCoursesData.loading" fill="clear" <ion-button *ngIf="!myOverviewBlock.prefetchCoursesData.loading" fill="clear"
(click)="myOverviewBlock.prefetchCourses()" (click)="myOverviewBlock.prefetchCourses()"

View File

@ -1,13 +1,7 @@
ion-item { ion-item {
--border-color: var(--core-more-item-border, var(--ion-border-color)); --border-color: var(--core-more-item-border, var(--ion-border-color));
ion-icon { > ion-icon[slot] {
color: var(--core-more-icon, rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.54)); color: var(--core-more-icon) !important;
}
}
:host-context(ion-app.ios) {
ion-item ion-icon {
color: var(--core-more-icon, inherit);
} }
} }

View File

@ -7,10 +7,10 @@
"cannotsyncoffline": "Cannot synchronise offline.", "cannotsyncoffline": "Cannot synchronise offline.",
"cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", "cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.",
"colorscheme": "Color Scheme", "colorscheme": "Color Scheme",
"colorscheme-system": "System default",
"colorscheme-system-notice": "System default mode will depend on your device support.",
"colorscheme-dark": "Dark", "colorscheme-dark": "Dark",
"colorscheme-light": "Light", "colorscheme-light": "Light",
"colorscheme-system": "System default",
"colorscheme-system-notice": "System default mode will depend on your device support.",
"compilationinfo": "Compilation info", "compilationinfo": "Compilation info",
"copyinfo": "Copy device info on the clipboard", "copyinfo": "Copy device info on the clipboard",
"cordovadevicemodel": "Cordova device model", "cordovadevicemodel": "Cordova device model",
@ -27,7 +27,7 @@
"disableall": "Disable notifications", "disableall": "Disable notifications",
"disabled": "Disabled", "disabled": "Disabled",
"disabledfeatures": "Disabled features", "disabledfeatures": "Disabled features",
"disallowed": "Disallowed", "disallowed": "Locked off",
"displayformat": "Display format", "displayformat": "Display format",
"enabledownloadsection": "Enable download sections", "enabledownloadsection": "Enable download sections",
"enablefirebaseanalytics": "Enable Firebase analytics", "enablefirebaseanalytics": "Enable Firebase analytics",

View File

@ -9,7 +9,7 @@ $gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default; $gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default; // Stroke $gray-300: #dee2e6 !default; // Stroke
$gray-400: #ced4da !default; $gray-400: #ced4da !default;
$gray-500: #8f959e !default; $gray-500: #8f959e !default; // Stroke on inputs
$gray-600: #6a737b !default; $gray-600: #6a737b !default;
$gray-700: #495057 !default; $gray-700: #495057 !default;
$gray-800: #343a40 !default; $gray-800: #343a40 !default;
@ -46,8 +46,8 @@ $warning: $yellow !default;
$success: $green !default; $success: $green !default;
$info: $blue !default; $info: $blue !default;
$light: $gray-100 !default; $light: $gray-100 !default;
$medium: $gray-600 !default; $medium: $gray-700 !default;
$dark: $gray-800 !default; $dark: $gray-900 !default;
$colors: ( $colors: (
primary: (light: $primary, dark: $primary), primary: (light: $primary, dark: $primary),

View File

@ -298,6 +298,10 @@ button,
min-width: var(--a11y-min-target-size); min-width: var(--a11y-min-target-size);
} }
ion-fab-button {
--box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12);
}
ion-button { ion-button {
margin: 4px 8px; margin: 4px 8px;
@ -321,7 +325,8 @@ ion-button.button-outline {
--border-width: var(--core-input-border-width); --border-width: var(--core-input-border-width);
--border-color: var(--core-input-stroke); --border-color: var(--core-input-stroke);
--background: var(--core-input-background); --background: var(--core-input-background);
--color: var(--text-color); --color: var(--core-input-text);
--ion-color-primary: var(--core-input-text);
} }
ion-button.button-solid { ion-button.button-solid {
@ -371,7 +376,8 @@ ion-button.button.button-clear.button-has-icon-only {
} }
ion-button.button.button-clear { ion-button.button.button-clear {
--color: var(--dark); --color: var(--core-input-text);
--ion-color-primary: var(--core-input-text);
} }
ion-button.button.button-solid, ion-button.button.button-solid,
@ -936,7 +942,7 @@ ion-select-popover ion-item.core-select-option-title {
ion-badge { ion-badge {
line-height: 1.1; line-height: 1.1;
padding: 4px 8px; padding: 2px 8px;
border-radius: var(--big-radius); border-radius: var(--big-radius);
} }
@ -1105,7 +1111,7 @@ ion-fab[core-fab] {
} }
ion-content.has-collapsible-footer ion-fab { ion-content.has-collapsible-footer ion-fab {
bottom: calc(var(--core-navigation-height, 0px) + 10px); bottom: calc(var(--core-collapsible-footer-height, 0px) + 10px);
@include core-transition(all, 200ms); @include core-transition(all, 200ms);
} }

View File

@ -36,18 +36,20 @@
--text-color: #{$text-color-dark}; --text-color: #{$text-color-dark};
--ion-text-color: var(--text-color); --ion-text-color: var(--text-color);
--ion-text-color-rgb: #{$text-color-dark-rgb}; --ion-text-color-rgb: #{$text-color-dark-rgb};
--subdued-text-color: var(--gray-400); --subdued-text-color: var(--medium);
--stroke: var(--gray-700); --stroke: var(--gray-700);
--contrast-background: black; --contrast-background: black;
--ion-card-color: var(--text-color); --ion-card-color: var(--text-color);
--ion-card-background: var(--ion-item-background); --ion-card-background: var(--ion-item-background);
--ion-card-border-color: var(--stroke);
--ion-border-color: var(--stroke); --ion-border-color: var(--stroke);
--ion-item-border-color: var(--stroke); --ion-item-border-color: var(--stroke);
--core-input-stroke: var(--gray-700); --core-input-stroke: var(--gray-600);
--core-input-text: var(--dark);
--core-input-background: var(--gray-900); --core-input-background: var(--gray-900);
ion-content { ion-content {
@ -68,7 +70,7 @@
--core-link-color: var(--info-tint); --core-link-color: var(--info-tint);
--core-header-toolbar-background: var(--gray-900); --core-header-toolbar-background: var(--gray-900);
--core-header-toolbar-color: var(--white); --core-header-toolbar-color: var(--text-color);
--core-header-toolbar-border-color: var(--stroke); --core-header-toolbar-border-color: var(--stroke);
--core-tabs-background: var(--gray-800); --core-tabs-background: var(--gray-800);
@ -80,10 +82,15 @@
--core-progressbar-text-color: var(--gray-100); --core-progressbar-text-color: var(--gray-100);
--ion-item-background: #{$ion-item-background-dark}; --ion-item-background: #{$ion-item-background-dark};
--ion-item-icon-color: var(--medium);
--ion-item-detail-icon-color: var(--dark);
--core-more-icon: var(--ion-item-icon-color);
--item-divider-background: var(--ion-item-background); --item-divider-background: var(--ion-item-background);
--item-divider-color: var(--text-color); --item-divider-color: var(--text-color);
--spacer-background: var(--gray-700); --spacer-background: var(--gray-700);
--ion-searchbar-background: var(--ion-background-color); --ion-searchbar-background: var(--ion-background-color);
--ion-searchbar-border-color: var(--core-input-stroke); --ion-searchbar-border-color: var(--core-input-stroke);
--ion-searchbar-color: var(--text-color); --ion-searchbar-color: var(--text-color);
@ -91,7 +98,7 @@
--core-search-box-background: var(--ion-background-color); --core-search-box-background: var(--ion-background-color);
--core-search-box-border-color: var(--core-input-stroke); --core-search-box-border-color: var(--core-input-stroke);
--core-search-box-color: var(--text-color); --core-search-box-color: var(--core-input-text);
--core-combobox-background: var(--core-input-background); --core-combobox-background: var(--core-input-background);
--core-combobox-color: var(--text-color); --core-combobox-color: var(--text-color);

View File

@ -69,7 +69,8 @@
--ion-background-color-rgb: #{$background-color-rgb}; --ion-background-color-rgb: #{$background-color-rgb};
--ion-border-color: var(--stroke); --ion-border-color: var(--stroke);
--core-input-stroke: var(--gray-400); --core-input-stroke: var(--gray-500);
--core-input-text: var(--dark);
--core-input-background: var(--ion-background-color); --core-input-background: var(--ion-background-color);
--core-input-radius: var(--small-radius); --core-input-radius: var(--small-radius);
--core-input-border-width: 1px; --core-input-border-width: 1px;
@ -86,7 +87,7 @@
--ion-text-color: var(--text-color); --ion-text-color: var(--text-color);
--ion-text-color-rgb: #{$text-color-rgb}; --ion-text-color-rgb: #{$text-color-rgb};
--subdued-text-color: var(--gray-700); --subdued-text-color: var(--medium);
--ion-card-color: var(--text-color); --ion-card-color: var(--text-color);
--ion-card-vertical-margin: 10px; --ion-card-vertical-margin: 10px;
@ -112,7 +113,7 @@
} }
--core-bottom-tabs-background: var(--white); --core-bottom-tabs-background: var(--white);
--core-bottom-tabs-color: var(--gray-700); --core-bottom-tabs-color: var(--dark);
--core-bottom-tabs-color-selected: var(--primary); --core-bottom-tabs-color-selected: var(--primary);
--core-bottom-tabs-background-selected: transparent; --core-bottom-tabs-background-selected: transparent;
--core-bottom-tabs-badge-color: var(--primary); --core-bottom-tabs-badge-color: var(--primary);
@ -135,7 +136,7 @@
--core-header-toolbar-background: var(--white); --core-header-toolbar-background: var(--white);
--core-header-toolbar-border-width: 0px; --core-header-toolbar-border-width: 0px;
--core-header-toolbar-border-color: var(--stroke); --core-header-toolbar-border-color: var(--stroke);
--core-header-toolbar-color: var(--gray-900); --core-header-toolbar-color: var(--text-color);
--core-header-toolbar-height: 48px; --core-header-toolbar-height: 48px;
html.ios { html.ios {
--core-header-toolbar-height: 48px; --core-header-toolbar-height: 48px;
@ -191,10 +192,10 @@
--core-search-box-background: var(--ion-background-color); --core-search-box-background: var(--ion-background-color);
--core-search-box-border-color: var(--core-input-stroke); --core-search-box-border-color: var(--core-input-stroke);
--core-search-box-border-radius: var(--core-input-radius); --core-search-box-border-radius: var(--core-input-radius);
--core-search-box-color: var(--text-color); --core-search-box-color: var(--core-input-text);
--core-combobox-background: var(--core-input-background); --core-combobox-background: var(--core-input-background);
--core-combobox-color: var(--text-color); --core-combobox-color: var(--core-input-text);
--core-combobox-border-color: var(--core-input-stroke); --core-combobox-border-color: var(--core-input-stroke);
--core-combobox-border-width: var(--core-input-border-width); --core-combobox-border-width: var(--core-input-border-width);
--core-combobox-radius: var(--core-input-radius); --core-combobox-radius: var(--core-input-radius);
@ -255,13 +256,19 @@
--core-progressbar-background: var(--primary-tint); --core-progressbar-background: var(--primary-tint);
--ion-item-background: #{$ion-item-background}; --ion-item-background: #{$ion-item-background};
--ion-item-detail-icon-color: var(--medium); --ion-item-icon-color: var(--medium);
--ion-item-detail-icon-color: var(--dark);
--ion-item-detail-icon-font-size: 20px; --ion-item-detail-icon-font-size: 20px;
--ion-item-detail-icon-opacity: 1; --ion-item-detail-icon-opacity: 1;
--core-more-icon: var(--ion-item-icon-color);
ion-item { ion-item {
--detail-icon-color: var(--ion-item-detail-icon-color); --detail-icon-color: var(--ion-item-detail-icon-color);
--detail-icon-font-size: var(--ion-item-detail-icon-font-size); --detail-icon-font-size: var(--ion-item-detail-icon-font-size);
--detail-icon-opacity: var(--ion-item-detail-icon-opacity); --detail-icon-opacity: var(--ion-item-detail-icon-opacity);
> ion-icon[slot] {
color: var(--ion-item-icon-color);
}
} }
--item-divider-min-height: calc(var(--a11y-min-target-size) + 8px); --item-divider-min-height: calc(var(--a11y-min-target-size) + 8px);
@ -309,13 +316,10 @@
--core-large-avatar-size: 90px; --core-large-avatar-size: 90px;
--core-avatar-size: var(--a11y-min-target-size); --core-avatar-size: var(--a11y-min-target-size);
--core-send-message-input-background: var(--gray-200);
--core-send-message-input-color: var(--gray-900);
--core-courseimage-on-course-size: 72px; --core-courseimage-on-course-size: 72px;
--core-courseimage-radius: var(--medium-radius); --core-courseimage-radius: var(--medium-radius);
--core-navigation-height: 48px; --core-collapsible-footer-height: 48px;
--core-navigation-background: var(--contrast-background); --core-navigation-background: var(--contrast-background);
--core-collapsible-footer-background: var(--contrast-background); --core-collapsible-footer-background: var(--contrast-background);