Skip to content

Commit 91bae8d

Browse files
committed
MOBILE-4910 mainmenu: Manage custom user menu items like main menu items
1 parent 7018cb6 commit 91bae8d

File tree

4 files changed

+269
-209
lines changed

4 files changed

+269
-209
lines changed

src/core/features/mainmenu/pages/more/more.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { Subscription } from 'rxjs';
1818
import { CoreSites } from '@services/sites';
1919
import { CoreQRScan } from '@services/qrscan';
2020
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/mainmenu-delegate';
21-
import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu';
21+
import { CoreMainMenu } from '../../services/mainmenu';
2222
import { CoreEventObserver, CoreEvents } from '@singletons/events';
2323
import { CoreNavigator } from '@services/navigator';
2424
import { Translate } from '@singletons';
@@ -28,6 +28,7 @@ import { CoreSharedModule } from '@/core/shared.module';
2828
import { CoreMainMenuUserButtonComponent } from '../../components/user-menu-button/user-menu-button';
2929
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
3030
import { CoreUrl } from '@singletons/url';
31+
import { CoreCustomMenu, CoreCustomMenuItem } from '@features/mainmenu/services/custommenu';
3132

3233
/**
3334
* Page that displays the more page of the app.
@@ -46,7 +47,7 @@ export default class CoreMainMenuMorePage implements OnInit, OnDestroy {
4647
handlers?: CoreMainMenuHandlerData[];
4748
handlersLoaded = false;
4849
showScanQR: boolean;
49-
customItems?: CoreMainMenuCustomItem[];
50+
customItems?: CoreCustomMenuItem[];
5051

5152
protected allHandlers?: CoreMainMenuHandlerData[];
5253
protected subscription!: Subscription;
@@ -58,7 +59,7 @@ export default class CoreMainMenuMorePage implements OnInit, OnDestroy {
5859
this.langObserver = CoreEvents.on(CoreEvents.LANGUAGE_CHANGED, () => this.loadCustomMenuItems());
5960

6061
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async () => {
61-
this.customItems = await CoreMainMenu.getCustomMenuItems();
62+
this.customItems = await CoreCustomMenu.getCustomMainMenuItems();
6263
}, CoreSites.getCurrentSiteId());
6364

6465
this.loadCustomMenuItems();
@@ -118,7 +119,7 @@ export default class CoreMainMenuMorePage implements OnInit, OnDestroy {
118119
* Load custom menu items.
119120
*/
120121
protected async loadCustomMenuItems(): Promise<void> {
121-
this.customItems = await CoreMainMenu.getCustomMenuItems();
122+
this.customItems = await CoreCustomMenu.getCustomMainMenuItems();
122123
}
123124

124125
/**
@@ -137,7 +138,7 @@ export default class CoreMainMenuMorePage implements OnInit, OnDestroy {
137138
*
138139
* @param item Item to open.
139140
*/
140-
openItem(item: CoreMainMenuCustomItem): void {
141+
openItem(item: CoreCustomMenuItem): void {
141142
CoreViewer.openIframeViewer(item.label, item.url);
142143
}
143144

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// (C) Copyright 2015 Moodle Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { Injectable } from '@angular/core';
16+
17+
import { CoreConstants, CoreLinkOpenMethod } from '@/core/constants';
18+
import { CoreLang, CoreLangFormat, CoreLangLanguage } from '@services/lang';
19+
import { Device, makeSingleton } from '@singletons';
20+
import { CorePlatform } from '@services/platform';
21+
import { CoreSites } from '@services/sites';
22+
import { CoreText } from '@singletons/text';
23+
24+
/**
25+
* Service that provides some features regarding custom main and user menu.
26+
*/
27+
@Injectable({ providedIn: 'root' })
28+
export class CoreCustomMenuService {
29+
30+
/**
31+
* Get a list of custom main menu items.
32+
*
33+
* @param siteId Site to get custom items from.
34+
* @returns List of custom menu items.
35+
*/
36+
async getCustomMainMenuItems(siteId?: string): Promise<CoreCustomMenuItem[]> {
37+
const customItems = await Promise.all([
38+
this.getCustomMenuItemsFromSite('tool_mobile_custommenuitems', siteId),
39+
this.getCustomItemsFromConfig(CoreConstants.CONFIG.customMainMenuItems),
40+
]);
41+
42+
return customItems.flat();
43+
}
44+
45+
/**
46+
* Get a list of custom user menu items.
47+
*
48+
* @param siteId Site to get custom items from.
49+
* @returns List of custom menu items.
50+
*/
51+
async getUserCustomMenuItems(siteId?: string): Promise<CoreCustomMenuItem[]> {
52+
const customItems = await Promise.all([
53+
this.getCustomMenuItemsFromSite('tool_mobile_customusermenuitems', siteId),
54+
this.getCustomItemsFromConfig(CoreConstants.CONFIG.customUserMenuItems),
55+
]);
56+
57+
return customItems.flat();
58+
}
59+
60+
/**
61+
* Get a list of custom menu items for a certain site.
62+
*
63+
* @param config Config key to get items from.
64+
* @param siteId Site ID. If not defined, current site.
65+
* @returns List of custom menu items.
66+
*/
67+
protected async getCustomMenuItemsFromSite(config: string, siteId?: string): Promise<CoreCustomMenuItem[]> {
68+
const site = await CoreSites.getSite(siteId);
69+
70+
const itemsString = site.getStoredConfig(config);
71+
if (!itemsString || typeof itemsString !== 'string') {
72+
// Setting not valid.
73+
return [];
74+
}
75+
76+
const map: CustomMenuItemsMap = {};
77+
const result: CoreCustomMenuItem[] = [];
78+
79+
let position = 0; // Position of each item, to keep the same order as it's configured.
80+
81+
// Add items to the map.
82+
const items = itemsString.split(/(?:\r\n|\r|\n)/);
83+
items.forEach((item) => {
84+
const values = item.split('|');
85+
const label = values[0] ? values[0].trim() : values[0];
86+
const url = values[1] ? values[1].trim() : values[1];
87+
const type = values[2] ? values[2].trim() : values[2];
88+
const lang = (values[3] ? values[3].trim() : values[3]) || 'none';
89+
let icon = values[4] ? values[4].trim() : values[4];
90+
91+
if (!label || !url || !type) {
92+
// Invalid item, ignore it.
93+
return;
94+
}
95+
96+
const id = `${url}#${type}`;
97+
if (!icon) {
98+
// Icon not defined, use default one.
99+
icon = type === CoreLinkOpenMethod.EMBEDDED
100+
? 'fas-expand' // @todo Find a better icon for embedded.
101+
: 'fas-link';
102+
}
103+
104+
if (!map[id]) {
105+
// New entry, add it to the map.
106+
map[id] = {
107+
url,
108+
type: type as CoreLinkOpenMethod,
109+
position,
110+
labels: {},
111+
};
112+
position++;
113+
}
114+
115+
map[id].labels[lang.toLowerCase()] = {
116+
label: label,
117+
icon: icon,
118+
};
119+
});
120+
121+
if (!position) {
122+
// No valid items found, stop.
123+
return result;
124+
}
125+
126+
const currentLangApp = await CoreLang.getCurrentLanguage();
127+
const currentLangLMS = CoreLang.formatLanguage(currentLangApp, CoreLangFormat.LMS);
128+
const fallbackLang = CoreConstants.CONFIG.default_lang || 'en';
129+
130+
// Get the right label for each entry and add it to the result.
131+
for (const id in map) {
132+
const entry = map[id];
133+
let data = entry.labels[currentLangApp]
134+
?? entry.labels[currentLangLMS]
135+
?? entry.labels[`${currentLangApp}_only`]
136+
?? entry.labels[`${currentLangLMS}_only`]
137+
?? entry.labels.none
138+
?? entry.labels[fallbackLang];
139+
140+
if (!data) {
141+
// No valid label found, get the first one that is not "_only".
142+
for (const lang in entry.labels) {
143+
if (!lang.includes('_only')) {
144+
data = entry.labels[lang];
145+
break;
146+
}
147+
}
148+
149+
if (!data) {
150+
// No valid label, ignore this entry.
151+
continue;
152+
}
153+
}
154+
155+
result[entry.position] = {
156+
url: entry.url,
157+
type: entry.type,
158+
label: data.label,
159+
icon: data.icon,
160+
};
161+
}
162+
163+
// Remove undefined values.
164+
return result.filter((entry) => entry !== undefined);
165+
}
166+
167+
/**
168+
* Get a list of custom menu items from config.
169+
*
170+
* @param items Items from config.
171+
* @returns List of custom menu items.
172+
*/
173+
protected async getCustomItemsFromConfig(items?: CoreCustomMenuLocalizedCustomItem[]): Promise<CoreCustomMenuItem[]> {
174+
if (!items) {
175+
return [];
176+
}
177+
178+
const currentLang = await CoreLang.getCurrentLanguage();
179+
180+
const fallbackLang = CoreConstants.CONFIG.default_lang || 'en';
181+
const replacements = {
182+
devicetype: '',
183+
osversion: Device.version,
184+
};
185+
186+
if (CorePlatform.isAndroid()) {
187+
replacements.devicetype = 'Android';
188+
} else if (CorePlatform.isIOS()) {
189+
replacements.devicetype = 'iPhone or iPad';
190+
} else {
191+
replacements.devicetype = 'Other';
192+
}
193+
194+
return items
195+
.filter(item => typeof item.label === 'string' || currentLang in item.label || fallbackLang in item.label)
196+
.map(item => ({
197+
...item,
198+
url: CoreText.replaceArguments(item.url, replacements, 'uri'),
199+
label: typeof item.label === 'string'
200+
? item.label
201+
: item.label[currentLang] ?? item.label[fallbackLang],
202+
}));
203+
}
204+
205+
}
206+
207+
export const CoreCustomMenu = makeSingleton(CoreCustomMenuService);
208+
209+
/**
210+
* Custom menu item.
211+
*/
212+
export interface CoreCustomMenuItem {
213+
/**
214+
* Type of the item: app, inappbrowser, browser or embedded.
215+
*/
216+
type: CoreLinkOpenMethod;
217+
218+
/**
219+
* Url of the item.
220+
*/
221+
url: string;
222+
223+
/**
224+
* Label to display for the item.
225+
*/
226+
label: string;
227+
228+
/**
229+
* Name of the icon to display for the item.
230+
*/
231+
icon: string;
232+
}
233+
234+
/**
235+
* Custom custom menu item with localized text.
236+
*/
237+
export type CoreCustomMenuLocalizedCustomItem = Omit<CoreCustomMenuItem, 'label'> & {
238+
label: string | Record<CoreLangLanguage, string>;
239+
};
240+
241+
/**
242+
* Map of custom menu items.
243+
*/
244+
type CustomMenuItemsMap = Record<string, {
245+
url: string;
246+
type: CoreLinkOpenMethod;
247+
position: number;
248+
labels: {
249+
[lang: string]: {
250+
label: string;
251+
icon: string;
252+
};
253+
};
254+
}>;

0 commit comments

Comments
 (0)