Source: App.vue

<template>
  <div id="q-app" style="min-height: 100vh;" class="nomy nopx nomx" @keydown.f9="translate">
    <q-layout view="hHh Lpr fFf" container style="height: 100vh" class="nomy nopx nomx">
      <q-header style="height: 40px;">
        <q-toolbar class="nopx" style="min-height:40px;">
          <q-btn flat @click="$store.drawer = !$store.drawer" dense icon="menu" />
          <q-toolbar-title class="text-subtitle1 nomy">AI4SoilHealth {{ $store.version
            }}
          </q-toolbar-title>
          <q-btn flat dense class="nomy" v-if="!$store.isOnline" icon="wifi_off" />
          {{ $store.userData && $store.userData.first_name > '' && $store.userData.last_name > '' ?
    (this.$q.screen.width
      >= 1024 ? `${$store.userData.first_name} ${$store.userData.last_name}` : $store.userData.first_name.charAt(0)
    +
    $store.userData.last_name.charAt(0)) :
    $t('Guest') }}
          <q-btn v-if="$keycloak.token" class="nomy" flat @click="$logout" dense icon="logout" />
          <q-btn v-else class="nomy" flat dense icon="login" @click="$keycloak.login()">
          </q-btn>
          <accessibility />
          <lang-switcher ref="langSwitcher" />
          <q-btn class="nomy" flat @click="toggleFullscreen" dense
            :icon="(fullscreen ? 'fullscreen_exit' : 'fullscreen')" />
        </q-toolbar>
      </q-header>

      <q-drawer style="top: 40px" v-model="$store.drawer" :width="$store.drawerWidth" bordered behavior="desktop"
        :breakpoint="breakpoint" :overlay="false">
        <q-scroll-area class="fit">
          <div v-if="isAdmin" class="row">
            <q-input v-model="treeFilter" dense style="width:160px">
              <template v-slot:prepend>
                <q-icon name="search"></q-icon>
              </template>
            </q-input>
            <q-btn flat dense icon="refresh" @click="getRoutes" />
          </div>
          <q-tree ref="tree" class="primary text-body2" :nodes="tree" node-key="path" no-connectors :filter="treeFilter"
            :default-expand-all="treeFilter.length > 0" v-model:selected="selected" v-if="tree.length > 0"
            @update:selected="selectionUpdated" />
        </q-scroll-area>
      </q-drawer>

      <q-scroll-area style="height: 100vh; max-width: 100vw;" :bar-style="{ width: '10px' }"
        :thumb-style="{ width: '0px' }">
        <q-page-container class="q-pt-none">
          <q-page>
            <router-view />
          </q-page>
        </q-page-container>
      </q-scroll-area>

    </q-layout>
    <popup v-if="$store.popup.show" @keydown.f9="translate" />
    <chart-popup v-if="$store.chart.show" />
    <help-dialog @keydown.f9="translate" />
    <task-progress v-if="$store.progress.show" />
  </div>
</template>
<script lang="js">
import { setCssVar } from 'quasar';
import ChartPopup from './components/chart-popup.vue';
import LangSwitcher from "./components/lang-switcher.vue";
import Accessibility from './components/accessibility.vue';
import Popup from './components/popup.vue';
import HelpDialog from './components/help-dialog.vue';
import PWAPrompt from './components/PWAPrompt.vue';
import TaskProgress from './components/task-progress.vue';

//import { shallowRef, markRaw } from "vue";
/**
 * The main component of the application.
 * Renders the layout and handles user interactions.
 *
 * @component
 * @example
 * <App />
 */
export default {
  name: "App",
  components: {
    LangSwitcher,
    Accessibility,
    Popup,
    PWAPrompt,
    Popup,
    HelpDialog,
    ChartPopup,
    TaskProgress,
  },
  data: () => ({
    selected: null,
    fullscreen: false,
    canInstall: false,
    breakpoint: 1024,
    treeFilter: '',
  }),
  /**
   *  The created lifecycle hook.
   * Checks if the server is online and calls the init method.
   */
  async created() {
    // window.addEventListener('online', () => {
    //   this.$store.isOnline = true;
    //   initKeycloak(this.$store.isOnline);
    // });

    // window.addEventListener('offline', () => {
    //   this.$store.isOnline = false;
    //   initKeycloak(this.$store.isOnline);
    // });
    // alert(navigator.onLine);
    console.log(`App: navigator.onLine: ${navigator.onLine}`);
    // console.log(`App: $store.isOnline: ${this.$store.isOnline}`);
    // this.$store.isOnline = navigator.onLine;
    if (this.$store.isOnline) {
      this.axios.API.get("Home/Ping", null)
        .then(response => {
          this.init();
        });
    } else {
      this.init();
    }
  },

  async mounted() {
    setCssVar("tooltip-fontsize", "12px");
    this.$store.drawer = this.$q.screen.width >= this.breakpoint;
    this.$store.userData = this.$q.localStorage.getItem("userData");

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.addEventListener('controllerchange', () => {
        this.$q.dialog({
          title: this.$t('Update'),
          message: this.$t('New version available. Reload now?'),
          cancel: true,
          persistent: true
        }).onOk(() => {
          window.location.reload();
        });
      });
    }
  },
  watch: {
    treeFilter(val) {
      if (val.length > 0) {
        this.$refs.tree.expandAll();
      }
    }
  },
  computed: {
    /**
     * Filters the tree based on the provided filter text.
     * @returns {Array} The filtered tree items.
     */
    treeFiltered() {
      // Filter the tree based on the treeFilter, recusively filtering the children
      return this.tree.filter((item) => {
        if (item.label.toLowerCase().includes(this.treeFilter.toLowerCase())) return true;
        if (item.children) {
          item.children = item.children.filter((child) => {
            if (child.label.toLowerCase().includes(this.treeFilter.toLowerCase())) return true;
            return false;
          });
          return item.children.length > 0;
        }
        return false;
      });
    },
    /**
     * Retrieves the tree data.
     */
    tree() {
      let root = this.$store.routes.filter((item) => !item.parent && item.active);

      let children = root.map(route => ({
        label: this.$t(route.title),
        name: route.name,
        path: route.path,
        icon: route.icon,
        iconColor: route.iconColor ?? "primary",
        offline: route.offline,
        public: route.public,
        children: this.getChildRoutes(route.name),
      }));
      children = children.filter(c => (this.$store.isOnline || c.offline));
      return children;
    },
  },
  methods: {
    /**
     * Initializes the application.
     * 
     * @returns {Promise<void>} A promise that resolves when the initialization is complete.
     */
    async init() {
      this.$store.localeOptions = await this.get("Home/GetLocaleOptions", null, true);
      this.$store.catalogs = await this.get("Home/GetCatalogs", null, true);
      this.$store.news = await this.get(`Home/GetNews/${this.$store.news.length}/10`, null, true);
      if (this.$keycloak.authenticated) {
        let ret = await this.get('Auth/GetUser');
        if (ret) {
          if (ret.agreement) {
            if (await this.confirmDialog(ret.agreement, this.$t('You have to accept the terms and conditions to continue:'),
              this.$t('Accept'), this.$t('Decline'))) {
              await this.post('Auth/AcceptAgreement');
              this.$store.userData = ret;
              this.$q.localStorage.set('userData', this.$store.userData);
            } else {
              this.$logout();
            }
          } else {
            this.$store.userData = ret;
            this.$q.localStorage.set('userData', this.$store.userData);
          }
        } else {
          this.$logout();
          //this.$store.userData = null;
          //this.$q.localStorage.remove('userData');
        }
      }
      this.$refs.langSwitcher.localeChanged();
    },
    /**
     * Updates the selection with the specified ID.
     * 
     * @param {number} id - The ID of the selection to update.
     * @returns {Promise<void>} - A promise that resolves when the selection is updated.
     */
    async selectionUpdated(id) {
      if (id == null) return;
      if (this.$store.formChanged) {
        if (!await this.confirmDialog(this.$t('Unsaved changes will be lost. Continue?'))) {
          this.$store.formChanged = false;
          return;
        }
        this.$store.formChanged = false;
      }
      let route = this.$store.routes.find((item) => item.path == id);
      if (route.component_name > "") {
        this.activateRoute(route);
        this.$store.drawer = this.$q.screen.width >= this.breakpoint;
      } else {
        this.$refs.tree.setExpanded(id, !this.$refs.tree.isExpanded(id));
      }
      this.selected = null;
    },

    /**
     * Retrieves the child routes for a given parent route.
     * 
     * @param {string} parentName - The name of the parent route.
     * @returns {Array} - An array of child routes.
     */
    getChildRoutes(parentName) {
      // Filter and return the child routes for the given parentName
      let children = this.$store.routes.filter((route) => route.parent === parentName && route.active);

      if (children.length === 0) return [];
      return children
        .map(route => ({
          label: route.title,
          name: route.name,
          path: route.path,
          icon: route.icon,
          iconColor: route.iconColor ?? "primary",
          offline: route.offline,
          children: this.getChildRoutes(route.path.substring(1)),
        }));
    },
    // checkMobileAndEnterFullscreen() {
    //   if (this.$q.platform.is.mobile) {
    //     document.documentElement.requestFullscreen();
    //   }
    // },
    toggleFullscreen() {
      if (this.fullscreen) {
        document.exitFullscreen();
      } else {
        document.documentElement.requestFullscreen();
      }
      this.fullscreen = !this.fullscreen;
    },
  },
}
</script>