|
| 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