Creación de aplicaciones de escritorio con Electron y Vue

Publicado: 2022-03-10
Resumen rápido ↬ Electron es un marco de software de código abierto desarrollado y mantenido por GitHub. Permite el desarrollo de aplicaciones GUI de escritorio utilizando tecnologías web. En este tutorial, Timi Omoyeni explica lo que debe tener en cuenta al crear una aplicación de escritorio con Vue.js utilizando Vue CLI Plugin Electron Builder.

JavaScript solía ser conocido como el lenguaje para crear sitios web y aplicaciones web, especialmente con algunos de sus marcos como React, Vue y Angular, pero con el tiempo (ya en 2009), fue posible que JavaScript se ejecutara fuera del navegador con el surgimiento de Node.js, un entorno de tiempo de ejecución JavaScript multiplataforma de código abierto que ejecuta código JavaScript fuera de un navegador web. Esto ha llevado a la capacidad de usar JavaScript para mucho más que solo aplicaciones web, y una de ellas es crear aplicaciones de escritorio usando Electron.js.

Electron le permite crear aplicaciones de escritorio con JavaScript puro al proporcionar un tiempo de ejecución con ricas API nativas (sistema operativo). Puede verlo como una variante del tiempo de ejecución de Node.js que se enfoca en aplicaciones de escritorio en lugar de servidores web.

En este tutorial, aprenderemos a crear aplicaciones de escritorio con Electron, también aprenderemos a usar Vuejs para crear aplicaciones de Electron.

Nota : Se requieren conocimientos básicos de Vue.js y Vue CLI para seguir este tutorial. Todo el código utilizado en este tutorial se puede encontrar en mi GitHub. ¡Siéntete libre de clonarlo y jugar con él!

¿Qué son las aplicaciones de escritorio?

Las aplicaciones de escritorio son aplicaciones que se ejecutan de forma independiente en computadoras de escritorio o portátiles. Son aplicaciones que realizan tareas específicas y se instalan únicamente con este fin.

Un ejemplo de una aplicación de escritorio es Microsoft Word, que se utiliza para crear y escribir documentos. Otros ejemplos de aplicaciones de escritorio comunes son los navegadores web, Visual Studio Code y Adobe Photoshop. Las aplicaciones de escritorio son diferentes de las aplicaciones web porque debe instalar la aplicación de escritorio para poder acceder a ella y hacer uso de ella, y a veces no necesitan acceso a Internet para que funcionen. Las aplicaciones web, por otro lado, se pueden acceder simplemente visitando la URL en la que está alojada dicha aplicación y siempre necesitan acceso a Internet antes de poder acceder a ellas.

Los ejemplos de marcos utilizados en la creación de aplicaciones de escritorio incluyen:

  1. Java
    Java es un lenguaje de programación de propósito general basado en clases, orientado a objetos y diseñado para tener la menor cantidad posible de dependencias de implementación. Su objetivo es permitir que los desarrolladores de aplicaciones escriban una vez y se ejecuten en cualquier lugar (WORA), lo que significa que el código Java compilado puede ejecutarse en todas las plataformas compatibles con Java sin necesidad de volver a compilarlo.
  2. Efectos de Java
    De acuerdo con su documentación oficial, es una plataforma de aplicaciones de cliente de próxima generación de código abierto para sistemas integrados, móviles y de escritorio construidos en Java.
  3. C#
    C# es un lenguaje de programación multiparadigma de propósito general que abarca disciplinas de programación orientadas a componentes, imperativa, imperativa, declarativa, funcional, genérica, orientada a objetos y tipificación fuerte.
  4. .NETO
    .NET es una plataforma de desarrollo gratuita, multiplataforma y de código abierto para crear muchos tipos diferentes de aplicaciones. Con .NET, puede usar múltiples lenguajes, editores y bibliotecas para crear aplicaciones web, móviles, de escritorio, juegos e IoT.
¡Más después del salto! Continúe leyendo a continuación ↓

¿Qué es el electrón?

Electron es un marco de código abierto para crear aplicaciones de escritorio. Anteriormente se conocía como 'Atom shell' y es desarrollado y mantenido por GitHub. Le permite escribir aplicaciones de escritorio multiplataforma utilizando HTML, CSS y JavaScript. Esto significa que puede crear aplicaciones de escritorio para Windows, MacOS y otras plataformas utilizando una base de código. Está basado en Node.js y Chromium. Los ejemplos de aplicaciones creadas con Electron incluyen el popular editor Atom, Visual Studio Code, Wordpress para escritorio y Slack.

Instalación

Puede instalar Electron en su proyecto usando NPM:

 npm install electron --save-dev

También puede instalarlo globalmente si va a trabajar mucho con aplicaciones electrónicas usando este comando:

 npm install electron -g

Creación de aplicaciones Vuejs para escritorio con Electron

Si está familiarizado con la creación de aplicaciones web con Vuejs, es posible crear aplicaciones de escritorio con Vuejs. Todo lo que necesita para esto es Vue CLI Plugin Electron Builder.

Complemento Vue CLI Electron Builder

Esta herramienta le permite crear aplicaciones Vue para escritorio con Electron, lo que significa que hace que su aplicación Vue funcione como una aplicación electrónica. Esto significa que su aplicación Vue, que posiblemente sea una aplicación web, se puede ampliar para trabajar en entornos de escritorio sin necesidad de crear una aplicación de escritorio separada en otro marco. Esto brinda a los desarrolladores de Vue la opción y el poder de ir más allá de la web. En el futuro, puede trabajar en esa idea que tiene y brindarles a los usuarios una opción de aplicación de escritorio, una que puede ejecutarse en Windows, macOS y Linux.

Para ver esto en acción, vamos a crear una aplicación de noticias usando la API de noticias. La aplicación proporcionará titulares de noticias de última hora y le permitirá buscar artículos de fuentes de noticias y blogs en toda la web con su API. Todo lo que necesita para comenzar con ellos es su clave API personal que se puede obtener desde aquí.

Construiremos una aplicación simple que ofrece lo siguiente:

  1. Una página que muestra los titulares principales y de última hora de un país seleccionado con la opción de elegir un país usando su punto final /top-headlines . La API de noticias proporciona noticias de una lista de países que admiten; encuentre la lista aquí.
  2. Noticias de una categoría seleccionada usando una combinación de su extremo /everything y un parámetro de consulta q con el que especificaremos nuestra categoría.

Después de obtener su clave API, podemos crear nuestra aplicación utilizando la CLI de Vue. Asegúrese de tener la CLI de Vue instalada en su sistema, si no la tiene, instálela usando este comando:

 npm install -g @vue/cli # OR yarn global add @vue/cli

Una vez hecho esto, cree su aplicación de noticias usando la CLI:

 vue create news-app

Obtendremos los datos de News API usando Axios para este tutorial, pero puede usar cualquier alternativa con la que se sienta más cómodo. Puede instalar Axios usando cualquiera de los siguientes comandos:

 //NPM npm install axios // YARN yarn add axios

El siguiente paso sería configurar una instancia de Axios para la configuración global en nuestra aplicación. Vamos a crear una carpeta de complementos en la carpeta src donde crearemos este archivo axios.js . Después de crear el archivo, agregue las siguientes líneas de código:

 import axios from "axios"; let baseURL = `https://newsapi.org/v2`; let apiKey = process.env.VUE_APP_APIKEY; const instance = axios.create({ baseURL: baseURL, timeout: 30000, headers: { "X-Api-Key": apiKey, }, }); export default instance;

Aquí, definimos nuestra baseURL y apiKey que obtuvimos de News API y las pasamos a una nueva instancia de Axios. Esta instancia acepta baseURL y apiKey junto con una propiedad de tiempo de timeout . La API de noticias requiere que agregue su clave de API cuando realiza una solicitud a su API y ofrece 3 formas de adjuntarla a su solicitud, pero aquí la agregamos a la propiedad X-Api-Key del encabezado, después de lo cual exportamos la instance . Una vez hecho esto, ahora podemos usar esta configuración para todas nuestras solicitudes de Axios en nuestra aplicación.

Cuando haya terminado, puede agregar el generador de complementos Electron con la CLI usando este comando:

 vue add electron-builder

Se le pedirá que seleccione su versión preferida de Electron, seleccioné la versión 9.0.0 porque es la última versión de Electron (en el momento de escribir este artículo).

Cuando haya terminado, ahora puede servir su aplicación usando este comando:

 Using Yarn(strongly recommended) yarn electron:serve OR NPM npm run electron:serve

Esto tomará algún tiempo para compilar y servir su aplicación. Cuando haya terminado, su aplicación se abrirá en su sistema, esto debería verse así:

estado abierto predeterminado de su aplicación electrónica
Estado de apertura automática de su aplicación de electrones. (Vista previa grande)

Si cierra las herramientas de desarrollo de su aplicación, debería verse así:

Anding página de su aplicación
Página de destino de su aplicación. (Vista previa grande)

Este complemento electrónico es muy útil y fácil de usar porque cada parte del desarrollo de esta aplicación funciona de la misma manera que una aplicación Vue. Esto significa que puede tener una base de código tanto para su aplicación web como para su aplicación de escritorio. Nuestra aplicación tendrá tres partes:

  1. Una página de destino que muestra las principales noticias de un país elegido al azar.
  2. Una página para mostrar las principales noticias del país elegido por el usuario.
  3. Una página que muestra las principales noticias de una categoría seleccionada por el usuario.

Para esto, vamos a necesitar un componente de encabezado para todos nuestros enlaces de navegación. Así que vamos a crear un archivo en la carpeta de componentes y nombrarlo header.vue , y luego agregarle las siguientes líneas de código:

 <template> <header class="header"> <div class="logo"> <div class="logo__container"> <img src="../assets/logo.png" alt="News app logo" class="logo__image" /> </div> <h1>News App</h1> </div> <nav class="nav"> <h4 class="nav__link"> <router-link to="/home">Home</router-link> </h4> <h4 class="nav__link"> <router-link to="/top-news">Top News</router-link> </h4> <h4 class="nav__link"> <router-link to="/categories">News By Category</router-link> </h4> </nav> </header> </template> <script> export default { name: "app-header", }; </script> <style> .header { display: flex; flex-wrap: wrap; justify-content: space-between; } .logo { display: flex; flex-wrap: nowrap; justify-content: space-between; align-items: center; height: 50px; } .logo__container { width: 50px; height: 50px; } .logo__image { max-width: 100%; max-height: 100%; } .nav { display: flex; flex-wrap: wrap; width: 350px; justify-content: space-between; } </style>

Aquí, creamos un componente de encabezado que tiene el nombre y el logotipo de nuestra aplicación (la imagen se puede encontrar en mi GitHub) junto con una sección de navegación que contiene enlaces a otras partes de nuestra aplicación. Lo siguiente sería importar esta página en nuestra página de diseño: App.vue para que podamos ver nuestro encabezado en cada página.

 <template> <div> <app-header /> <router-view /> </div> </template> <script> import appHeader from "@/components/Header.vue"; export default { name: "layout", components: { appHeader, }, }; </script> <style> @import url("https://fonts.googleapis.com/css2?family=Abel&family=Staatliches&display=swap"); html, #app { min-height: 100vh; } #app { font-family: "Abel", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; background-color: #fff; } #app h1 { font-family: "Staatliches", cursive; } a { font-weight: bold; color: #2c3e50; text-decoration: none; } a:hover { text-decoration: underline; } a.router-link-exact-active { color: #42b983; } </style>

Aquí, reemplazamos el contenido predeterminado en la sección de plantilla con nuestro componente de encabezado recién creado después de haberlo importado y declarado en la sección de script. Finalmente, agregamos algo de estilo para toda la aplicación en la sección de estilo.

Ahora, si intentamos ver nuestra aplicación, debería verse así:

página de destino vacía
Página de destino vacía. (Vista previa grande)

El siguiente paso sería agregar contenido a nuestro archivo Home.vue . Esta página albergaría la primera sección de nuestra aplicación; Las mejores noticias de un país seleccionado al azar. Actualice su archivo Home.vue con las siguientes líneas de código:

 <template> <section class="home"> <h1>Welcome to News App</h1> <h4>Displaying Top News from {{ countryInfo.name }}</h4> <div class="articles__div" v-if="articles"> <news-card v-for="(article, index) in articles" :key="index" :article="article" ></news-card> </div> </section> </template> <script> import { mapActions, mapState } from "vuex"; import NewsCard from "../components/NewsCard"; export default { data() { return { articles: "", countryInfo: "", }; }, components: { NewsCard, }, mounted() { this.fetchTopNews(); }, computed: { ...mapState(["countries"]), }, methods: { ...mapActions(["getTopNews"]), async fetchTopNews() { let countriesLength = this.countries.length; let countryIndex = Math.floor( Math.random() * (countriesLength - 1) + 1 ); this.countryInfo = this.countries[countryIndex]; let { data } = await this.getTopNews( this.countries[countryIndex].value ); this.articles = data.articles; }, }, }; </script> <style> .articles__div { display: flex; flex-wrap: wrap; justify-content: center; } </style>

En la sección de script de este archivo, importamos mapState y mapActions de Vuex, que usaremos más adelante en este archivo. También importamos un componente NewsCard (lo crearemos a continuación) que mostraría todos los titulares de noticias en esta página. Luego hacemos uso del método fetchTopNews para obtener las últimas noticias de un país seleccionado al azar de la variedad de countries en nuestra tienda. Este país se pasa a nuestra acción getTopNews , esto se agregaría a la baseURL como una consulta para un país como baseURL/top-news?country=${randomCountry} . Una vez hecho esto, repasamos estos datos y los pasamos al accesorio del article de nuestro componente Newscard en la sección de plantillas. También tenemos un párrafo que indica de qué país es la noticia principal.

Lo siguiente sería configurar nuestro componente NewsCard que mostrará esta noticia. Cree un nuevo archivo dentro de su carpeta de componentes , asígnele el nombre NewsCard.vue y agréguele las siguientes líneas de código:

 <template> <section class="news"> <div class="news__section"> <h1 class="news__title"> <a class="article__link" :href="article.url" target="_blank"> {{ article.title }} </a> </h1> <h3 class="news__author" v-if="article.author">{{ article.author }}</h3> <!-- <p class="article__paragraph">{{ article.description }}</p> --> <h5 class="article__published">{{ new Date(article.publishedAt) }}</h5> </div> <div class="image__container"> <img class="news__img" src="../assets/logo.png" :data-src="article.urlToImage" :alt="article.title" /> </div> </section> </template> <script> export default { name: "news-card", props: { article: Object, }, mounted() { this.lazyLoadImages(); }, methods: { lazyLoadImages() { const images = document.querySelectorAll(".news__img"); const options = { // If the image gets within 50px in the Y axis, start the download. root: null, // Page as root rootMargin: "0px", threshold: 0.1, }; const fetchImage = (url) => { return new Promise((resolve, reject) => { const image = new Image(); image.src = url; image.onload = resolve; image.onerror = reject; }); }; const loadImage = (image) => { const src = image.dataset.src; fetchImage(src).then(() => { image.src = src; }); }; const handleIntersection = (entries) => { entries.forEach((entry) => { if (entry.intersectionRatio > 0) { loadImage(entry.target); } }); }; // The observer for the images on the page const observer = new IntersectionObserver(handleIntersection, options); images.forEach((img) => { observer.observe(img); }); }, }, }; </script> <style> .news { width: 100%; display: flex; flex-direction: row; align-items: flex-start; max-width: 550px; box-shadow: 2px 1px 7px 1px #eee; padding: 20px 5px; box-sizing: border-box; margin: 15px 5px; border-radius: 4px; } .news__section { width: 100%; max-width: 350px; margin-right: 5px; } .news__title { font-size: 15px; text-align: left; margin-top: 0; } .news__author { font-size: 14px; text-align: left; font-weight: normal; } .article__published { text-align: left; } .image__container { width: 100%; max-width: 180px; max-height: 180px; } .news__img { transition: max-width 300ms cubic-bezier(0.4, 0, 1, 1), max-height 300ms cubic-bezier(0.4, 0, 1, 1); max-width: 150px; max-height: 150px; } .news__img:hover { max-width: 180px; max-height: 180px; } .article__link { text-decoration: none; color: inherit; } </style>

Aquí, mostramos los datos pasados ​​a este componente usando el accesorio de objeto del article . También tenemos un método que carga de forma diferida las imágenes adjuntas a cada artículo. Este método recorre la cantidad de imágenes de artículos que tenemos y las carga de forma diferida cuando se vuelven visibles. Finalmente, tenemos estilos dirigidos a este componente en la sección de estilo.

Lo siguiente será montar nuestra tienda para que podamos empezar a recibir las últimas novedades. Agregue las siguientes líneas de código a su archivo index.js :

 import Vue from "vue"; import Vuex from "vuex"; import axios from "../plugins/axios"; Vue.use(Vuex); const store = new Vuex.Store({ state: { countries: [{ name: "United States of America", value: "us", }, { name: "Nigeria", value: "ng", }, { name: "Argentina", value: "ar", }, { name: "Canada", value: "ca", }, { name: "South Africa", value: "za", }, ], categories: [ "entertainment", "general", "health", "science", "business", "sports", "technology", ], }, mutations: {}, actions: { async getTopNews(context, country) { let res = await axios({ url: `/top-headlines?country=${country}`, method: "GET", }); return res; }, }, }); export default store;

Estamos agregando dos propiedades a nuestra tienda, una de estas propiedades es countries . Esta propiedad contiene una matriz de objetos de países. También tenemos la propiedad de categories ; esto contiene una variedad de categorías disponibles en la API de noticias. Al lector le gustará la libertad de ver las principales noticias de países y categorías específicos; esto también será necesario en más de una parte de la aplicación y es por eso que estamos haciendo uso de la tienda. En la sección de acciones de nuestra tienda, tenemos un método getTopNews que obtiene las noticias más importantes de un país (este país se pasó del componente que llamó a esta acción).

En este punto, si abrimos nuestra aplicación, deberíamos ver nuestra página de destino que se ve así:

Página de inicio actualizada
Página de destino actualizada. (Vista previa grande)

El archivo background.js

Este archivo es el punto de entrada de Electron a su aplicación. Controla todas las configuraciones similares a las de la aplicación de escritorio para esta aplicación. El estado predeterminado de este archivo se puede encontrar en mi GitHub.

En este archivo, tenemos algunas configuraciones predefinidas establecidas para la aplicación, como la height y el width predeterminados para su aplicación. Echemos un vistazo a algunas de las cosas que puede hacer en este archivo.

Habilitación de las herramientas de desarrollo de Vuejs

De forma predeterminada, tiene acceso a las herramientas de desarrollo en Electron, pero no está habilitado después de la instalación. Esto se debe a un error existente en Windows 10, por lo que si abre su archivo background.js , encontrará un código comentado con comentarios que indican por qué están comentados:

 // Install Vue Devtools // Devtools extensions are broken in Electron 6.0.0 and greater // See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info // Electron will not launch with Devtools extensions installed on Windows 10 with dark mode // If you are not using Windows 10 dark mode, you may uncomment these lines // In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines // try { // await installVueDevtools() // } catch (e) { // console.error('Vue Devtools failed to install:', e.toString()) // }

Entonces, si no está afectado por este error, puede descomentar el bloque try/catch y también buscar installVueDevtools en este mismo archivo (línea 5) y también descomentarlo. Una vez hecho esto, su aplicación se reiniciará automáticamente, y cuando verifique sus herramientas de desarrollo, debería ver Vuejs Devtools.

Vuejs en devtools
Vuejs en devtools. (Vista previa grande)

Selección de un icono personalizado para su aplicación

De manera predeterminada, el ícono de Electron está configurado como el ícono predeterminado para su aplicación y, la mayoría de las veces, probablemente desee configurar su propio ícono personalizado. Para hacer esto, mueva su ícono a su carpeta pública y cámbiele el nombre a icon.png . Lo siguiente que debe hacer sería agregar la dependencia requerida, electron-icon-builder .

Puede instalarlo usando cualquiera de los siguientes comandos:

 // With Yarn: yarn add --dev electron-icon-builder // or with NPM: npm install --save-dev electron-icon-builder

Una vez hecho esto, puede ejecutar este siguiente comando. Convertirá su ícono en formato Electron e imprimirá lo siguiente en su consola cuando esté hecho.

información generada en la terminal
Información generada en la terminal. (Vista previa grande)

Lo siguiente sería establecer la opción de icono en el archivo background.js . Esta opción va dentro de la opción BrowserWindow que se importa de Electron . Para hacer esto, actualice BrowserWindow para que se vea así:

 // Add this to the top of your file /* global __static */ // import path import path from 'path' // Replace win = new BrowserWindow({ width: 800, height: 600 }) // With win = new BrowserWindow({ width: 800, height: 600, icon: path.join(__static, 'icon.png') })

Ahora, si ejecutamos yarn run electron:build y vemos nuestra aplicación, deberíamos ver el ícono actualizado que se usa como el ícono de la aplicación, pero no cambia en el desarrollo. Este problema ayuda a abordar una solución manual en macOS.

Configuración del título para su aplicación

Notará que el título de su aplicación está configurado con el nombre de la aplicación (aplicación de noticias en este caso) y tendremos que cambiarlo. Para hacer eso, tenemos que agregar una propiedad de title al método BrowserWindow en nuestro archivo background.js como este:

 win = new BrowserWindow({ width: 600, height: 500, title: "News App", icon: path.join(__static, "icon.png"), webPreferences: { // Use pluginOptions.nodeIntegration, leave this alone // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, }, });

Aquí, estamos configurando el título de nuestra aplicación como 'Aplicación de noticias'. Pero si su archivo index.html tiene un título seleccionado o su título no cambia a este, intente agregar este código a su archivo:

 win.on("page-title-updated", (event) => event.preventDefault());

Estamos escuchando un evento que se activa cuando nuestro title se actualiza desde BrowserWindow . Cuando se activa este evento, le decimos a Electron que no actualice el título con el que se encuentra en el archivo index.html .

Otra cosa que podría valer la pena cambiar es el nombre del productName , esto controla qué nombre aparece cuando pasa el mouse sobre el ícono de su aplicación o cómo su computadora reconoce la aplicación. En este momento, el nombre de nuestra aplicación es Electron . Para cambiar este nombre en producción, cree un archivo vue.config.js y agréguele las siguientes líneas de código:

 module.exports = { pluginOptions: { electronBuilder: { builderOptions: { productName: "News App", }, }, }, };

Aquí, definimos productName como 'Aplicación de noticias' para que cuando ejecutemos el comando de compilación de nuestra aplicación, el nombre cambie de 'Electron' a 'Aplicación de noticias'.

Construcción multiplataforma

De forma predeterminada, cuando ejecuta el comando de compilación, la aplicación que se crea depende de la plataforma en la que se ejecuta. Esto significa que si ejecuta el comando de compilación en Linux, la aplicación que se crea será una aplicación de escritorio de Linux. Lo mismo se aplica a otras plataformas (macOS y Windows). Pero Electron viene con la opción de especificar una plataforma (o dos plataformas) que desea generar. Las opciones disponibles son:

  1. mac
  2. win
  3. linux

Entonces, para compilar la versión de Windows de su aplicación, ejecute el siguiente comando:

 // NPM npm electron:build -- --win nsis // YARN yarn electron:build --win nsis

Conclusión

La solicitud completa se puede encontrar en mi GitHub. La documentación oficial de Electron brinda información y una guía que lo ayuda a personalizar su aplicación de escritorio de la forma que desee. Algunas de las cosas que probé pero que no están incluidas en este tutorial son:

  1. Personalización de su base en macOS: https://www.electronjs.org/docs/tutorial/macos-dock.
  2. Configuración redimensionable, maximizable y muchas más: https://github.com/electron/electron/blob/master/docs/api/browser-window.md#new-browserwindowoptions.

Entonces, si está buscando hacer mucho más con su aplicación Electron, sus documentos oficiales son un buen lugar para comenzar.

Recursos Relacionados

  1. Node.jshttps://en.wikipedia.org/wiki/Node.js
  2. Java (lenguaje de programación)https://en.wikipedia.org/wiki/Java_(lenguaje_de_programación)
  3. Electron (marco de software)
  4. JavaFX 14
  5. electrones
  6. Documentación de electrones
  7. Complemento Vue CLI Electron Builder
  8. Carga diferida de imágenes para el rendimiento con Intersection Observer de Chris Nwamba
  9. axios
  10. Primeros pasos con Axios en Nuxt https://www.smashingmagazine.com/2020/05/getting-started-axios-nuxt/) por Timi Omoyeni