Merge pull request #3137 from crazyserver/MOBILE-3833

Mobile 3833
main
Dani Palou 2022-02-24 10:42:52 +01:00 committed by GitHub
commit 56a3287aff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 300 additions and 289 deletions

View File

@ -0,0 +1,13 @@
:host {
--mod-icon-filter: brightness(0);
core-mod-icon {
background: transparent;
margin: 0;
--filter: var(--mod-icon-filter);
}
}
:host-context(body.dark) {
--mod-icon-filter: brightness(0) invert(1);
}

View File

@ -28,6 +28,7 @@ import { CoreNavigator } from '@services/navigator';
@Component({
selector: 'addon-block-activitymodules',
templateUrl: 'addon-block-activitymodules.html',
styleUrls: ['activitymodules.scss'],
})
export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent implements OnInit {
@ -96,16 +97,13 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
// Sort the modnames alphabetically.
modFullNames = CoreUtils.sortValues(modFullNames);
for (const modName in modFullNames) {
let icon: string;
const iconModName = modName === 'resources' ? 'page' : modName;
if (modName === 'resources') {
icon = await CoreCourse.getModuleIconSrc('page', modIcons['page']);
} else {
icon = await CoreCourseModuleDelegate.getModuleIconSrc(modName, modIcons[modName]);
}
const icon = await CoreCourseModuleDelegate.getModuleIconSrc(iconModName, modIcons[iconModName]);
this.entries.push({
icon: icon,
icon,
iconModName,
name: modFullNames[modName],
modName,
});
@ -145,4 +143,5 @@ type AddonBlockActivityModuleEntry = {
icon: string;
name: string;
modName: string;
iconModName: string;
};

View File

@ -5,7 +5,7 @@
</ion-item-divider>
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<ion-item class="ion-text-wrap" *ngFor="let entry of entries" detail="true" button (click)="gotoCoureListModType(entry)">
<core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.modName" [showAlt]="false">
<core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.iconModName" [showAlt]="false">
</core-mod-icon>
<ion-label>{{ entry.name }}</ion-label>
</ion-item>

View File

@ -23,6 +23,7 @@ import { CoreConstants, ModPurpose } from '@/core/constants';
import { AddonModForumIndexComponent } from '../../components/index';
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
import { CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreIonicColorNames } from '@singletons/colors';
/**
* Handler to support forum modules.
@ -58,7 +59,7 @@ export class AddonModForumModuleHandlerService extends CoreModuleHandlerBase imp
const data = await super.getData(module, courseId);
if ('afterlink' in module && !!module.afterlink) {
data.extraBadgeColor = '';
data.extraBadgeColor = undefined;
const match = />(\d+)[^<]+/.exec(module.afterlink);
data.extraBadge = match ? Translate.instant('addon.mod_forum.unreadpostsnumber', { $a : match[1] }) : '';
} else {
@ -112,7 +113,7 @@ export class AddonModForumModuleHandlerService extends CoreModuleHandlerBase imp
}
data.extraBadge = Translate.instant('core.loading');
data.extraBadgeColor = 'light';
data.extraBadgeColor = CoreIonicColorNames.DARK;
await CoreUtils.ignoreErrors(AddonModForum.invalidateForumData(courseId));
@ -120,7 +121,7 @@ export class AddonModForumModuleHandlerService extends CoreModuleHandlerBase imp
// Handle unread posts.
const forum = await AddonModForum.getForum(courseId, moduleId, { siteId });
data.extraBadgeColor = '';
data.extraBadgeColor = undefined;
data.extraBadge = forum.unreadpostscount
? Translate.instant(
'addon.mod_forum.unreadpostsnumber',
@ -129,7 +130,7 @@ export class AddonModForumModuleHandlerService extends CoreModuleHandlerBase imp
: '';
} catch {
// Ignore errors.
data.extraBadgeColor = '';
data.extraBadgeColor = undefined;
data.extraBadge = '';
}
}

View File

@ -109,7 +109,7 @@
<ion-item class="ion-text-wrap addon-mod_h5pactivity-result-table-header">
<ion-label>
<ion-row class="ion-align-items-center">
<ion-col class="ion-text-center">{{ result.optionslabel }}</ion-col>
<ion-col class="ion-text-start">{{ result.optionslabel }}</ion-col>
<ion-col class="ion-text-center">{{ result.correctlabel }}</ion-col>
<ion-col class="ion-text-center">{{ result.answerlabel }}</ion-col>
</ion-row>
@ -118,7 +118,7 @@
<ion-item *ngFor="let option of result.options" class="ion-text-wrap addon-mod_h5pactivity-result-table-row">
<ion-label>
<ion-row class="ion-align-items-center">
<ion-col class="ion-text-center">
<ion-col class="ion-text-start">
<core-format-text [text]="option.description" [component]="component" [componentId]="cmId"
contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
</core-format-text>

View File

@ -20,6 +20,7 @@ import { CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreFileHelper } from '@services/file-helper';
import { CoreSites } from '@services/sites';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
@ -95,7 +96,14 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
this.getResourceData(module, courseId, handlerData).then((extra) => {
handlerData.extraBadge = extra;
handlerData.extraBadgeColor = '';
return;
}).catch(() => {
// Ignore errors.
});
this.getIconSrc(module).then((icon) => {
handlerData.icon = icon;
return;
}).catch(() => {
@ -214,6 +222,36 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
return extra.join(' ');
}
/**
* @inheritdoc
*/
async getIconSrc(module?: CoreCourseModuleData): Promise<string | undefined> {
if (!module) {
return;
}
if (CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('4.0')) {
return await CoreCourse.getModuleIconSrc(module.modname, module.modicon);
}
let mimetypeIcon = '';
if (module.contentsinfo) {
// No need to use the list of files.
const mimetype = module.contentsinfo.mimetypes[0];
if (mimetype) {
mimetypeIcon = CoreMimetypeUtils.getMimetypeIcon(mimetype);
}
} else if (module.contents && module.contents[0]) {
const files = module.contents;
const file = files[0];
mimetypeIcon = CoreMimetypeUtils.getFileIcon(file.filename || '');
}
return await CoreCourse.getModuleIconSrc(module.modname, module.modicon, mimetypeIcon);
}
/**
* @inheritdoc
*/

View File

@ -1,7 +1,6 @@
@import "~theme/globals";
:host {
--core-format-text-background-gradient-rgb: var(--background-rgb, #{$ion-item-background-rgb});
--course-storage-max-activity-height: 120px;
ion-card.section ion-card-header {
@ -23,8 +22,8 @@
min-height: var(--course-storage-max-activity-height);
position: absolute;
@include position(0, 0, null, 0);
background: -webkit-linear-gradient(top, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 30px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 20px));
background: linear-gradient(to bottom, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 30px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 20px));
background: -webkit-linear-gradient(top, rgba(var(--background-gradient-rgb), 0) calc(100% - 30px), rgba(var(--background-gradient-rgb), 1) calc(100% - 20px));
background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - 30px), rgba(var(--background-gradient-rgb), 1) calc(100% - 20px));
z-index: 6;
pointer-events: none;
}

View File

@ -1,62 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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.
import { Injectable } from '@angular/core';
import { CoreCourseOptionsMenuHandler, CoreCourseOptionsMenuHandlerData } from '@features/course/services/course-options-delegate';
import { CoreCourseAnyCourseDataWithOptions } from '@features/courses/services/courses';
import { makeSingleton } from '@singletons';
/**
* Handler to inject an option into course menu so that user can get to the manage storage page.
*/
@Injectable( { providedIn: 'root' })
export class AddonStorageManagerCourseMenuHandlerService implements CoreCourseOptionsMenuHandler {
name = 'AddonStorageManager';
priority = 500;
isMenuHandler = true;
/**
* @inheritdoc
*/
async isEnabledForCourse(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
getMenuDisplayData(
course: CoreCourseAnyCourseDataWithOptions,
): CoreCourseOptionsMenuHandlerData {
return {
icon: 'fas-cloud-download-alt',
title: 'addon.storagemanager.coursedownloads',
page: 'storage/' + course.id,
pageParams: {
title: course.displayname ?? course.fullname,
},
class: 'addon-storagemanager-coursemenu-handler',
};
}
}
export const AddonStorageManagerCourseMenuHandler = makeSingleton(AddonStorageManagerCourseMenuHandlerService);

View File

@ -14,12 +14,10 @@
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing';
import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate';
import { AddonStorageManagerCourseMenuHandler } from './services/handlers/course-menu';
import { AddonStorageManagerSettingsHandler } from './services/handlers/settings';
const routes: Routes = [
@ -41,7 +39,6 @@ const routes: Routes = [
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreCourseOptionsDelegate.registerHandler(AddonStorageManagerCourseMenuHandler.instance);
CoreSettingsDelegate.registerHandler(AddonStorageManagerSettingsHandler.instance);
},
},

View File

@ -1,6 +1,6 @@
<div [class.core-loading-container]="loading || !safeUrl" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}">
<core-navbar-buttons slot="end" append *ngIf="initialized && showFullscreenOnToolbar">
<core-navbar-buttons slot="end" prepend *ngIf="initialized && showFullscreenOnToolbar">
<ion-button fill="clear" (click)="toggleFullscreen()"
[attr.aria-label]="(fullscreen ? 'core.disablefullscreen' : 'core.fullscreen') | translate">
<ion-icon *ngIf="!fullscreen" name="fas-expand" slot="icon-only" aria-hidden="true"></ion-icon>

View File

@ -64,8 +64,8 @@ export class CoreCollapsibleItemDirective implements OnInit {
}
this.maxHeight = this.maxHeight < defaultMaxHeight ? defaultMaxHeight : this.maxHeight;
if (!this.maxHeight || (window.innerWidth > 576 && window.innerHeight > 576)) {
// Do not collapse on big screens.
if (!this.maxHeight) {
// Do not collapse.
return;
}
@ -91,9 +91,6 @@ export class CoreCollapsibleItemDirective implements OnInit {
*/
protected calculateHeight(): void {
// @todo: Work on calculate this height better.
if (!this.maxHeight) {
return;
}
// Remove max-height (if any) to calculate the real height.
const initialMaxHeight = this.element.style.maxHeight;
@ -117,7 +114,11 @@ export class CoreCollapsibleItemDirective implements OnInit {
this.toggleExpandEnabled = enable;
this.element.classList.toggle('collapsible-enabled', enable);
if (!enable || this.element.querySelector('ion-button.collapsible-toggle')) {
if (!enable || this.element.querySelector('ion-button.collapsible-toggle')) {
this.element.style.maxHeight = !enable || this.expanded
? ''
: this.maxHeight + 'px';
return;
}
@ -128,6 +129,7 @@ export class CoreCollapsibleItemDirective implements OnInit {
const toggleText = document.createElement('span');
toggleText.classList.add('collapsible-toggle-text');
toggleText.classList.add('sr-only');
toggleButton.appendChild(toggleText);
const expandArrow = document.createElement('span');

View File

@ -279,23 +279,28 @@ export class CoreFormatTextDirective implements OnChanges {
*/
protected setExpandButtonEnabled(enable: boolean): void {
this.toggleExpandEnabled = enable;
this.element.classList.toggle('core-text-formatted', enable);
this.element.classList.toggle('collapsible-enabled', enable);
if (!enable || this.element.querySelector('ion-button.collapsible-toggle')) {
this.element.style.maxHeight = !enable || this.expanded
? ''
: this.maxHeight + 'px';
if (!enable || this.element.querySelector('ion-button.core-format-text-toggle')) {
return;
}
// Add expand/collapse buttons
const toggleButton = document.createElement('ion-button');
toggleButton.classList.add('core-format-text-toggle');
toggleButton.classList.add('collapsible-toggle');
toggleButton.setAttribute('fill', 'clear');
const toggleText = document.createElement('span');
toggleText.classList.add('core-format-text-toggle-text');
toggleText.classList.add('collapsible-toggle-text');
toggleText.classList.add('sr-only');
toggleButton.appendChild(toggleText);
const expandArrow = document.createElement('span');
expandArrow.classList.add('core-format-text-arrow');
expandArrow.classList.add('collapsible-toggle-arrow');
toggleButton.appendChild(expandArrow);
this.element.appendChild(toggleButton);
@ -313,12 +318,12 @@ export class CoreFormatTextDirective implements OnChanges {
expand = !this.expanded;
}
this.expanded = expand;
this.element.classList.toggle('core-text-format-expanded', expand);
this.element.classList.toggle('core-text-format-collapsed', !expand);
this.element.classList.toggle('collapsible-expanded', expand);
this.element.classList.toggle('collapsible-collapsed', !expand);
this.element.style.maxHeight = expand ? '' : this.maxHeight + 'px';
const toggleButton = this.element.querySelector('ion-button.core-format-text-toggle');
const toggleText = toggleButton?.querySelector('.core-format-text-toggle-text');
const toggleButton = this.element.querySelector('ion-button.collapsible-toggle');
const toggleText = toggleButton?.querySelector('.collapsible-toggle-text');
if (!toggleButton || !toggleText) {
return;
}
@ -396,8 +401,7 @@ export class CoreFormatTextDirective implements OnChanges {
this.element.classList.add('core-disable-media-adapt');
this.contentSpan.innerHTML = ''; // Remove current contents.
if (this.maxHeight && result.div.innerHTML != '' &&
(window.innerWidth < 576 || window.innerHeight < 576)) { // Don't collapse in big screens.
if (this.maxHeight && result.div.innerHTML != '') {
// Move the children to the current element to be able to calculate the height.
CoreDomUtils.moveChildren(result.div, this.contentSpan);

View File

@ -33,16 +33,35 @@ ion-item.item.item-current {
--background: var(--primary);
--color: var(--primary-contrast);
border: 0;
ion-badge {
border: 1px solid var(--primary-contrast);
}
::ng-deep ion-icon {
color: var(--primary-contrast);
}
}
ion-icon.restricted {
font-size: 14px;
}
ion-item.item.divider.section {
--padding-start: 0px;
&.item-current {
ion-badge {
border: 1px solid var(--primary-contrast);
}
ion-icon.expandable-status-icon {
color: var(--primary-contrast);
&:hover {
background: var(--primary-shade);
}
}
}
ion-icon.expandable-status-icon {
padding: 13px;
margin: 3px;
border-radius: 50%;
&:hover {
background: var(--gray-300);
}
}
}

View File

@ -5,21 +5,24 @@
<ng-container *ngIf="completion.istrackeduser">
<ng-container *ngFor="let rule of details">
<ion-chip *ngIf="rule.statuscomplete" color="success" role="listitem" [attr.aria-label]="rule.accessibleDescription">
<ion-chip *ngIf="rule.statuscomplete" color="success" role="listitem" [attr.aria-label]="rule.accessibleDescription"
class="completioninfo completion_complete">
<ion-icon name="fas-check" [attr.aria-label]="'core.course.completion_automatic:done' | translate "></ion-icon>
<ion-label>
{{ rule.rulevalue.description }}
</ion-label>
</ion-chip>
<ion-chip *ngIf="rule.statuscompletefail" color="danger" role="listitem" [attr.aria-label]="rule.accessibleDescription">
<ion-chip *ngIf="rule.statuscompletefail" color="danger" role="listitem" [attr.aria-label]="rule.accessibleDescription"
class="completioninfo completion_fail">
<ion-icon name="fas-times" [attr.aria-label]="'core.course.completion_automatic:failed' | translate "></ion-icon>
<ion-label>
{{ rule.rulevalue.description }}
</ion-label>
</ion-chip>
<ion-chip *ngIf="rule.statusincomplete" color="dark" role="listitem" [attr.aria-label]="rule.accessibleDescription">
<ion-chip *ngIf="rule.statusincomplete" color="dark" role="listitem" [attr.aria-label]="rule.accessibleDescription"
class="completioninfo completion_incomplete">
<ion-icon name="fas-edit" [attr.aria-label]="'core.course.completion_automatic:todo' | translate "></ion-icon>
<ion-label>
{{ rule.rulevalue.description }}
@ -29,7 +32,7 @@
</ng-container>
<ng-container *ngIf="!completion.istrackeduser">
<ion-chip *ngFor="let rule of details" role="listitem">
<ion-chip *ngFor="let rule of details" role="listitem" class="core-module-completion-todo">
<ion-icon name="fas-edit" [attr.aria-label]="'core.course.completion_automatic:todo' | translate "></ion-icon>
<ion-label>
{{ rule.rulevalue.description }}

View File

@ -1,12 +0,0 @@
:host {
.core-module-automatic-completion-conditions {
ion-badge {
font-weight: normal;
margin-right: 5px;
&[color="medium"] {
color: black;
}
}
}
}

View File

@ -36,7 +36,6 @@ import { Translate } from '@singletons';
@Component({
selector: 'core-course-module-completion',
templateUrl: 'core-course-module-completion.html',
styleUrls: ['module-completion.scss'],
})
export class CoreCourseModuleCompletionComponent extends CoreCourseModuleCompletionBaseComponent {

View File

@ -37,7 +37,7 @@
{{ 'core.course' | translate}}
</p>
<p>
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="courseId">
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</p>
</ion-label>
@ -58,7 +58,7 @@
<ion-item lines="full" class="ion-text-wrap">
<ion-label>
<h2>
<ion-icon name="fam-cloud-done" aria-hidden="true"></ion-icon>
<ion-icon name="fas-cloud-download-alt" aria-hidden="true"></ion-icon>
{{ 'addon.storagemanager.downloads' | translate }}
</h2>
</ion-label>
@ -81,7 +81,7 @@
</ion-item>
<ion-button fill="outline" expand="block" *ngIf="canPrefetch && displayOptions.displayPrefetch" class="ion-text-wrap"
(click)="prefetch()" color="primary" [disabled]="prefetchDisabled">
<ion-icon *ngIf="!prefetchLoading" name="fam-cloud-done" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="!prefetchLoading" name="fas-cloud-download-alt" slot="start" aria-hidden="true"></ion-icon>
<ion-spinner *ngIf="prefetchLoading" slot="start" aria-hidden="true"></ion-spinner>
<ion-label>
{{ 'core.download' | translate }}

View File

@ -249,7 +249,13 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
* Fetch course.
*/
protected async fetchCourse(): Promise<void> {
this.course = await CoreCourses.getUserCourse(this.courseId, true);
// Fix that.
try {
this.course = await CoreCourses.getUserCourse(this.courseId, true);
} catch {
// The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
this.course = await CoreCourses.getCourse(this.courseId);
}
}
/**

View File

@ -71,7 +71,7 @@
</core-format-text>
<!-- Module completion. Only auto conditions-->
<core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata"
<core-course-module-completion *ngIf="autoCompletionTodo && module.uservisible" [completion]="module.completiondata"
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions">
</core-course-module-completion>

View File

@ -2,10 +2,14 @@
:host {
--horizontal-margin: 10px;
--vertical-margin: 10px;
ion-card {
margin-left: var(--horizontal-margin);
margin-right: var(--horizontal-margin);
margin: var(--vertical-margin) var(--horizontal-margin);
}
ion-item {
--padding-start: 12px;
}
ion-item.core-module-main-item {
@ -84,4 +88,7 @@
@include margin-horizontal(null, 8px);
}
.core-course-module-info ::ng-deep core-course-module-completion .core-module-automatic-completion-conditions .completioninfo.completion_complete {
display: none;
}
}

View File

@ -20,7 +20,7 @@ import {
CoreCourseModuleCompletionData,
CoreCourseSection,
} from '@features/course/services/course-helper';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
import {
CoreCourseModulePrefetchDelegate,
@ -55,6 +55,8 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled.
prefetchStatusIcon = ''; // Module prefetch status icon.
prefetchStatusText = ''; // Module prefetch status text.
autoCompletionTodo = false;
protected prefetchHandler?: CoreCourseModulePrefetchHandler;
protected moduleStatusObserver?: CoreEventObserver;
@ -73,10 +75,18 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title;
const completionStatus = this.showCompletionConditions && this.module.completiondata?.isautomatic &&
this.module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC
? this.module.completiondata.state
: undefined;
this.autoCompletionTodo = completionStatus == CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE ||
completionStatus == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL;
this.hasInfo = !!(
this.module.description ||
(this.showActivityDates && this.module.dates && this.module.dates.length) ||
(this.module.completiondata && this.showCompletionConditions && this.module.completiondata.isautomatic) ||
(this.autoCompletionTodo) ||
(this.module.visible === 0 && (!this.section || this.section.visible)) ||
(this.module.visible !== 0 && this.module.isStealth) ||
(this.module.availabilityinfo)

View File

@ -1,4 +1,7 @@
<core-navbar-buttons slot="end">
<core-navbar-buttons slot="end" prepend>
<ion-button fill="clear" (click)="gotoCourseDownloads()" [attr.aria-label]="'addon.storagemanager.coursedownloads' | translate">
<ion-icon name="fas-cloud-download-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-navbar-buttons>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!dataLoaded || !displayRefresher" (ionRefresh)="doRefresh($event.target)">

View File

@ -29,7 +29,6 @@ import {
} from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreCourseOptionsMenuHandlerToDisplay } from '@features/course/services/course-options-delegate';
import { CoreCourseSync, CoreCourseSyncProvider } from '@features/course/services/sync';
import { CoreCourseFormatComponent } from '../../components/course-format/course-format';
import {
@ -54,7 +53,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
sections?: CoreCourseSection[];
sectionId?: number;
sectionNumber?: number;
courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
dataLoaded = false;
downloadCourseEnabled = false;
moduleId?: number;
@ -368,8 +366,16 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
}
}
gotoCourseDownloads(): void {
CoreNavigator.navigateToSitePath(
`storage/${this.course.id}`,
{ params: { title: this.course.fullname } },
);
}
/**
* Page destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.isDestroyed = true;

View File

@ -26,14 +26,23 @@
<ng-container *ngIf="course">
<ion-item class="ion-text-wrap">
<ion-label>
<p *ngIf="course.categoryname">
<core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid">
<p *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
class="core-course-shortname">
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</p>
<h2>
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</h2>
<ion-chip color="brand" *ngIf="course.categoryname" class="core-course-category ion-text-nowrap">
<span class="sr-only">{{ 'core.courses.aria:coursecategory' | translate }}</span>
<ion-label>
<core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid">
</core-format-text>
</ion-label>
</ion-chip>
<div class="core-course-progress" *ngIf="progress !== undefined">
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">

View File

@ -21,10 +21,6 @@
<img [src]="imageThumb" core-external-content alt="" />
</ion-avatar>
<ion-label>
<p *ngIf="category">
<core-format-text [text]="category" contextLevel="coursecat" [contextInstanceId]="course!.categoryid">
</core-format-text>
</p>
<h1>{{ title }}</h1>
<div class="core-course-progress" *ngIf="progress !== undefined">
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">

View File

@ -238,7 +238,6 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
// Get the title to display initially.
this.title = CoreCourseFormatDelegate.getCourseTitle(this.course);
this.category = 'categoryname' in this.course ? this.course.categoryname : '';
if ('overviewfiles' in this.course) {
this.imageThumb = this.course.overviewfiles?.[0]?.fileurl;

View File

@ -25,6 +25,7 @@ import { CoreSites } from '@services/sites';
import { makeSingleton } from '@singletons';
import { CoreCourseModuleData } from './course-helper';
import { CoreNavigationOptions } from '@services/navigator';
import { CoreIonicColorNames } from '@singletons/colors';
/**
* Interface that all course module handlers must implement.
@ -146,7 +147,7 @@ export interface CoreCourseModuleHandlerData {
/**
* The color of the extra badge. Default: primary.
*/
extraBadgeColor?: string;
extraBadgeColor?: CoreIonicColorNames;
/**
* Whether to display a button to download/refresh the module if it's downloadable.

View File

@ -70,7 +70,8 @@
class="core-course-category core-course-additional-info ion-text-nowrap">
<span class="sr-only">{{ 'core.courses.aria:coursecategory' | translate }}</span>
<ion-label>
<core-format-text [text]="course.categoryname"></core-format-text>
<core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid">
</core-format-text>
</ion-label>
</ion-chip>
</div>

View File

@ -34,6 +34,10 @@
margin-bottom: 16px;
}
form .core-username.ios {
--inner-border-width: 0 0 1px 0;
}
form .item,
form .item ion-label {
--background: var(--core-login-input-background);

View File

@ -15,7 +15,7 @@
<core-loading [hideUntil]="userLoaded">
<ion-list *ngIf="user">
<ion-item class="ion-text-center core-user-profile-maininfo ion-text-wrap" lines="full">
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="!canChangeProfilePicture">
<ion-button class="edit-avatar" *ngIf="canChangeProfilePicture" (click)="changeProfilePicture()"
[attr.aria-label]="'core.user.newpicture' | translate" fill="clear" color="dark">
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>

View File

@ -4,13 +4,11 @@
core-format-text {
--core-format-text-background: var(--background, var(--ion-item-background));
--core-format-text-background-gradient-rgb: var(--background-rgb, #{$ion-item-background-rgb});
--core-format-text-viewer-icon-background: rgba(255, 255, 255, .5);
--core-format-text-loader-shine: 251,251,251;
}
body.dark core-format-text {
--core-format-text-background-gradient-rgb: var(--background-rgb, #{$ion-item-background-dark-rgb});
--core-format-text-viewer-icon-background: rgba(0, 0, 0, .5);
--core-format-text-loader-shine: 90,90,90;
}
@ -51,12 +49,12 @@ core-format-text {
display: inline;
}
.core-format-text-toggle {
.collapsible-toggle {
display: none !important;
}
}
.core-format-text-toggle {
.collapsible-toggle {
display: none;
}
@ -84,81 +82,21 @@ core-format-text {
}
// This is to allow clicks in radio/checkbox content.
&.core-text-formatted {
&.collapsible-enabled {
cursor: pointer;
pointer-events: auto;
.core-format-text-toggle {
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
z-index: 7;
text-transform: none;
text-align: end;
font-size: 14px;
background-color: var(--core-format-text-background);
color: var(--text-color);
margin: 0;
.core-format-text-arrow {
width: var(--a11y-min-target-size);
height: var(--a11y-min-target-size);
background-position: center;
background-repeat: no-repeat;
background-size: 14px 14px;
@include core-transition(transform, 500ms);
@include push-arrow-color(626262, true);
@include darkmode() {
@include push-arrow-color(ffffff, true);
}
}
}
&.core-text-format-collapsed {
overflow: hidden;
min-height: 50px;
.core-format-text-arrow {
transform: rotate(90deg);
}
&:before {
content: '';
height: 100%;
position: absolute;
@include position(null, 0, 0, 0);
background: -webkit-linear-gradient(top, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 60px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 40px));
background: linear-gradient(to bottom, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 60px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 40px));
z-index: 6;
}
}
&.core-text-format-expanded {
max-height: none !important;
padding-bottom: 50px; // So the Show less button can fit.
.core-format-text-arrow {
transform: rotate(-90deg);
}
}
@include collapsible-item();
}
}
@if ($core-format-text-never-shorten) {
&[maxHeight],
&[ng-reflect-max-height] {
&.core-text-formatted.core-text-format-expanded {
&.collapsible-enabled.collapsible-expanded {
max-height: none !important;
.core-format-text-toggle {
.collapsible-toggle {
display: none !important;
}

View File

@ -219,9 +219,84 @@
--horizontal-margin: 6px;
}
}
}
@mixin collapsible-item() {
--display-toggle: none;
.collapsible-toggle {
display: var(--display-toggle);
}
@include media-breakpoint-down(sm) {
&.collapsible-enabled {
position:relative;
--display-toggle: block;
.collapsible-toggle {
position: absolute;
@include position (null, 0, 0, null);
text-align: center;
z-index: 7;
text-transform: none;
font-size: 14px;
font-weight: normal;
background-color: var(--collapsible-toggle-background);
color: var(--collapsible-toggle-text);
min-height: var(--a11y-min-target-size);
min-width: var(--a11y-min-target-size);
--border-radius: var(--huge-radius);
border-radius: var(--border-radius);
--padding-start: 0px;
--padding-end: 0px;
margin: 0px;
.collapsible-toggle-arrow {
width: var(--a11y-min-target-size);
height: var(--a11y-min-target-size);
background-position: center;
background-repeat: no-repeat;
background-size: 14px 14px;
@include core-transition(transform, 500ms);
@include push-arrow-color(626262, true);
@include darkmode() {
@include push-arrow-color(ffffff, true);
}
}
}
&.collapsible-collapsed {
overflow: hidden;
min-height: calc(var(--collapsible-min-button-height) + 12px);
.collapsible-toggle-arrow {
transform: rotate(90deg);
}
&:before {
content: '';
height: 100%;
position: absolute;
@include position(null, 0, 0, 0);
background: -webkit-linear-gradient(top, rgba(var(--background-gradient-rgb), 0) calc(100% - 56px), rgba(var(--background-gradient-rgb), 1) calc(100% - 5px));
background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - 56px), rgba(var(--background-gradient-rgb), 1) calc(100% - 5px));
z-index: 6;
}
}
&.collapsible-expanded {
max-height: none !important;
padding-bottom: var(--collapsible-min-button-height); // So the Show less button can fit.
.collapsible-toggle-arrow {
transform: rotate(-90deg);
}
}
}
}
}
// Color mixins.
@function get_brightness($color) {

View File

@ -164,6 +164,9 @@ ion-header {
--ion-toolbar-color: var(--core-header-toolbar-color);
--border-radius: var(--huge-radius);
}
ion-back-button::part(text) {
display: none;
}
.button.button-clear,
.button.button-solid {
@ -813,6 +816,7 @@ ion-card {
border-color: var(--border-color);
box-shadow: var(--box-shadow);
border-radius: var(--border-radius);
margin: var(--ion-card-vertical-margin) var(--ion-card-horizontal-margin);
ion-item:only-child {
--inner-border-width: 0px;
@ -983,6 +987,12 @@ ion-chip {
min-height: 24px;
height: auto;
// Chips are not currently clickable.
&.ion-activatable {
cursor: auto;
pointer-events: none;
}
&.ion-color {
background: var(--ion-color-tint);
&.chip-outline {
@ -1145,6 +1155,10 @@ ion-item.item-lines-inset {
--border-width: 0px;
}
ion-item.item-input.ios {
--inner-border-width: 0 0 1px 0;
}
// Fake item.
div.fake-ion-item {
position: relative;
@ -1425,77 +1439,7 @@ ion-grid.core-no-grid > ion-row {
}
[collapsible-item] {
--collapsible-display-toggle: none;
--collapsible-toggle-background: var(--ion-item-background);
--collapsible-min-button-height: 44px;
.collapsible-toggle {
display: var(--collapsible-display-toggle);
}
&.collapsible-enabled {
--collapsible-display-toggle: block;
.collapsible-toggle {
display: var(--collapsible-display-toggle);
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
z-index: 7;
text-transform: none;
text-align: end;
font-size: 14px;
background-color: var(--collapsible-toggle-background);
color: var(--text-color);
margin: 0;
.collapsible-toggle-arrow {
width: var(--a11y-min-target-size);
height: var(--a11y-min-target-size);
background-position: center;
background-repeat: no-repeat;
background-size: 14px 14px;
@include core-transition(transform, 500ms);
@include push-arrow-color(626262, true);
@include darkmode() {
@include push-arrow-color(ffffff, true);
}
}
}
&.collapsible-collapsed {
overflow: hidden;
min-height: calc(var(--collapsible-min-button-height) + 12);
.collapsible-toggle-arrow {
transform: rotate(90deg);
}
&:before {
content: '';
height: 100%;
position: absolute;
@include position(null, 0, 0, 0);
background: -webkit-linear-gradient(top, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 60px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 40px));
background: linear-gradient(to bottom, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 60px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 40px));
z-index: 6;
}
}
&.collapsible-expanded {
max-height: none !important;
padding-bottom: var(--collapsible-min-button-height); // So the Show less button can fit.
.collapsible-toggle-arrow {
transform: rotate(-90deg);
}
}
}
@include collapsible-item();
}
ion-header.no-title {

View File

@ -97,6 +97,10 @@
--core-combobox-color: var(--text-color);
--core-combobox-border-color: var(--core-input-stroke);
--collapsible-toggle-background: var(--light);
--background-gradient-rgb: #{$ion-item-background-dark-rgb};
--core-login-background: var(--gray-900);
--core-login-text-color: var(--white);
--core-login-input-background: var(--core-login-background);

View File

@ -88,6 +88,8 @@
--subdued-text-color: var(--gray-700);
--ion-card-color: var(--text-color);
--ion-card-vertical-margin: 10px;
--ion-card-horizontal-margin: 10px;
ion-card {
--border-width: 1px;
--border-style: solid;
@ -286,6 +288,12 @@
--selected-item-color: var(--primary);
--selected-item-border-width: 5px;
--collapsible-toggle-background: var(--light);
--collapsible-min-button-height: 44px;
--collapsible-toggle-text: var(--text-color);
--background-gradient-rgb: #{$ion-item-background-rgb};
--core-login-background: var(--white);
--core-login-text-color: var(--gray-900);
--core-login-input-background: var(--white);