Skip to content

Commit 527c449

Browse files
committed
MOBILE-4910 mainmenu: Manage custom user menu items like main menu items
1 parent 56f8b9d commit 527c449

File tree

4 files changed

+272
-209
lines changed

4 files changed

+272
-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: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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+
protected static readonly CUSTOM_MAIN_MENU_ITEMS_CONFIG = 'tool_mobile_custommenuitems';
31+
protected static readonly CUSTOM_USER_MENU_ITEMS_CONFIG = 'tool_mobile_customusermenuitems';
32+
33+
/**
34+
* Get a list of custom main menu items.
35+
*
36+
* @param siteId Site to get custom items from.
37+
* @returns List of custom menu items.
38+
*/
39+
async getCustomMainMenuItems(siteId?: string): Promise<CoreCustomMenuItem[]> {
40+
const customItems = await Promise.all([
41+
this.getCustomMenuItemsFromSite(CoreCustomMenuService.CUSTOM_MAIN_MENU_ITEMS_CONFIG, siteId),
42+
this.getCustomItemsFromConfig(CoreConstants.CONFIG.customMainMenuItems),
43+
]);
44+
45+
return customItems.flat();
46+
}
47+
48+
/**
49+
* Get a list of custom user menu items.
50+
*
51+
* @param siteId Site to get custom items from.
52+
* @returns List of custom menu items.
53+
*/
54+
async getUserCustomMenuItems(siteId?: string): Promise<CoreCustomMenuItem[]> {
55+
const customItems = await Promise.all([
56+
this.getCustomMenuItemsFromSite(CoreCustomMenuService.CUSTOM_USER_MENU_ITEMS_CONFIG, siteId),
57+
this.getCustomItemsFromConfig(CoreConstants.CONFIG.customUserMenuItems),
58+
]);
59+
60+
return customItems.flat();
61+
}
62+
63+
/**
64+
* Get a list of custom menu items for a certain site.
65+
*
66+
* @param config Config key to get items from.
67+
* @param siteId Site ID. If not defined, current site.
68+
* @returns List of custom menu items.
69+
*/
70+
protected async getCustomMenuItemsFromSite(config: string, siteId?: string): Promise<CoreCustomMenuItem[]> {
71+
const site = await CoreSites.getSite(siteId);
72+
73+
const itemsString = site.getStoredConfig(config);
74+
if (!itemsString || typeof itemsString !== 'string') {
75+
// Setting not valid.
76+
return [];
77+
}
78+
79+
const map: CustomMenuItemsMap = {};
80+
const result: CoreCustomMenuItem[] = [];
81+
82+
let position = 0; // Position of each item, to keep the same order as it's configured.
83+
84+
// Add items to the map.
85+
const items = itemsString.split(/(?:\r\n|\r|\n)/);
86+
items.forEach((item) => {
87+
const values = item.split('|');
88+
const label = values[0] ? values[0].trim() : values[0];
89+
const url = values[1] ? values[1].trim() : values[1];
90+
const type = values[2] ? values[2].trim() : values[2];
91+
const lang = (values[3] ? values[3].trim() : values[3]) || 'none';
92+
let icon = values[4] ? values[4].trim() : values[4];
93+
94+
if (!label || !url || !type) {
95+
// Invalid item, ignore it.
96+
return;
97+
}
98+
99+
const id = `${url}#${type}`;
100+
if (!icon) {
101+
// Icon not defined, use default one.
102+
icon = type === CoreLinkOpenMethod.EMBEDDED
103+
? 'fas-expand' // @todo Find a better icon for embedded.
104+
: 'fas-link';
105+
}
106+
107+
if (!map[id]) {
108+
// New entry, add it to the map.
109+
map[id] = {
110+
url,
111+
type: type as CoreLinkOpenMethod,
112+
position,
113+
labels: {},
114+
};
115+
position++;
116+
}
117+
118+
map[id].labels[lang.toLowerCase()] = {
119+
label: label,
120+
icon: icon,
121+
};
122+
});
123+
124+
if (!position) {
125+
// No valid items found, stop.
126+
return result;
127+
}
128+
129+
const currentLangApp = await CoreLang.getCurrentLanguage();
130+
const currentLangLMS = CoreLang.formatLanguage(currentLangApp, CoreLangFormat.LMS);
131+
const fallbackLang = CoreConstants.CONFIG.default_lang || 'en';
132+
133+
// Get the right label for each entry and add it to the result.
134+
for (const id in map) {
135+
const entry = map[id];
136+
let data = entry.labels[currentLangApp]
137+
?? entry.labels[currentLangLMS]
138+
?? entry.labels[`${currentLangApp}_only`]
139+
?? entry.labels[`${currentLangLMS}_only`]
140+
?? entry.labels.none
141+
?? entry.labels[fallbackLang];
142+
143+
if (!data) {
144+
// No valid label found, get the first one that is not "_only".
145+
for (const lang in entry.labels) {
146+
if (!lang.includes('_only')) {
147+
data = entry.labels[lang];
148+
break;
149+
}
150+
}
151+
152+
if (!data) {
153+
// No valid label, ignore this entry.
154+
continue;
155+
}
156+
}
157+
158+
result[entry.position] = {
159+
url: entry.url,
160+
type: entry.type,
161+
label: data.label,
162+
icon: data.icon,
163+
};
164+
}
165+
166+
// Remove undefined values.
167+
return result.filter((entry) => entry !== undefined);
168+
}
169+
170+
/**
171+
* Get a list of custom menu items from config.
172+
*
173+
* @param items Items from config.
174+
* @returns List of custom menu items.
175+
*/
176+
protected async getCustomItemsFromConfig(items?: CoreCustomMenuLocalizedCustomItem[]): Promise<CoreCustomMenuItem[]> {
177+
if (!items) {
178+
return [];
179+
}
180+
181+
const currentLang = await CoreLang.getCurrentLanguage();
182+
183+
const fallbackLang = CoreConstants.CONFIG.default_lang || 'en';
184+
const replacements = {
185+
devicetype: '',
186+
osversion: Device.version,
187+
};
188+
189+
if (CorePlatform.isAndroid()) {
190+
replacements.devicetype = 'Android';
191+
} else if (CorePlatform.isIOS()) {
192+
replacements.devicetype = 'iPhone or iPad';
193+
} else {
194+
replacements.devicetype = 'Other';
195+
}
196+
197+
return items
198+
.filter(item => typeof item.label === 'string' || currentLang in item.label || fallbackLang in item.label)
199+
.map(item => ({
200+
...item,
201+
url: CoreText.replaceArguments(item.url, replacements, 'uri'),
202+
label: typeof item.label === 'string'
203+
? item.label
204+
: item.label[currentLang] ?? item.label[fallbackLang],
205+
}));
206+
}
207+
208+
}
209+
210+
export const CoreCustomMenu = makeSingleton(CoreCustomMenuService);
211+
212+
/**
213+
* Custom menu item.
214+
*/
215+
export interface CoreCustomMenuItem {
216+
/**
217+
* Type of the item: app, inappbrowser, browser or embedded.
218+
*/
219+
type: CoreLinkOpenMethod;
220+
221+
/**
222+
* Url of the item.
223+
*/
224+
url: string;
225+
226+
/**
227+
* Label to display for the item.
228+
*/
229+
label: string;
230+
231+
/**
232+
* Name of the icon to display for the item.
233+
*/
234+
icon: string;
235+
}
236+
237+
/**
238+
* Custom custom menu item with localized text.
239+
*/
240+
export type CoreCustomMenuLocalizedCustomItem = Omit<CoreCustomMenuItem, 'label'> & {
241+
label: string | Record<CoreLangLanguage, string>;
242+
};
243+
244+
/**
245+
* Map of custom menu items.
246+
*/
247+
type CustomMenuItemsMap = Record<string, {
248+
url: string;
249+
type: CoreLinkOpenMethod;
250+
position: number;
251+
labels: {
252+
[lang: string]: {
253+
label: string;
254+
icon: string;
255+
};
256+
};
257+
}>;

0 commit comments

Comments
 (0)