Source: stores/search.js

/*
 * Copyright © 2024-2025 The CTAN Team and individual authors
 *
 * This file is distributed under the 3-clause BSD license.
 * See file LICENSE for details.
 */
import { defineStore } from 'pinia'

const OPTS = [
  {
    icon: 'mdi-package-variant-closed',
    id: 'pkg',
    letter: 'P',
    value: true
  },
  {
    icon: 'mdi-tag-outline',
    id: 'topic',
    letter: 'T',
    value: true
  },
  {
    icon: 'mdi-account-badge-outline',
    id: 'author',
    letter: 'A',
    value: true
  },
  {
    icon: 'mdi-gavel',
    id: 'license',
    letter: 'L',
    value: true
  },
  {
    icon: 'mdi-folder-outline',
    id: 'file',
    letter: 'F',
    value: false
  },
  {
    icon: 'mdi-help-circle-outline',
    id: 'help',
    letter: 'S',
    value: true
  },
  {
    icon: 'mdi-mirror',
    id: 'mirror',
    letter: 'M',
    value: false
  }
]

/**
 * This is the search store.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
export const useSearchStore = defineStore('search', {
  state: () => ({
    /**
     * The search term.
     */
    filter: '',
    /**
     * The search history contains previous searches.
     * The elements have the form
     * [id, terms, options]
     */
    history: [],
    /**
     * 
     */
    loaded: false,
    /**
     * The list of options.
     */
    opts: OPTS,
    /**
     * Flags for the sections to be searched.
     */
    options: OPTS.reduce(function(map, obj) {
      map[obj.id] = obj.value
      return map
    }, {}),
    /**
     * Icons for the sections to be searched.
     */
    optIcons: OPTS.reduce(function(map, obj) {
      map[obj.letter] = obj.icon
      map[obj.id] = obj.icon
      return map
    }, {}),
    /**
     * The last search term.
     */
    recentFilter: '',
    /**
     * Indicator for an update request.
     */
    update: 0,
    /**
     * The indicator whether the search bar is opened.
     */
    visible: false
  }),

  getters: {
    /**
     * Translate the options flags into a compressed single-string form.
     */
    compressed: (state) =>
      (state.options.pkg ? 'P' : '') +
      (state.options.topic ? 'T' : '') +
      (state.options.author ? 'A' : '') +
      (state.options.file ? 'F' : '') +
      (state.options.license ? 'L' : '') +
      (state.options.mirror ? 'M' : '') +
      (state.options.help ? 'S' : ''),
    /**
     * Translate the options flags into a list.
     */
    list: (state) =>
      ['pkg', 'author', 'topic', 'license', 'file', 'mirror', 'help']
        .filter(it => state.options[it])
  },

  actions: {
    /**
     * Insert the current search to the beginnng of the history.
     * If the history contains this search already then the double is deleted.
     * The history is clipped to 32 elements.
     */
    addToHistory () {
      this.recentFilter = this.filter
      let filter = this.filter
      if (filter === null) {
        filter = ''
      }
      const id = this.searchId(this.filter, this.compressed)
      const i = this.history.findIndex(it => it[0] === id)
      if (i >= 0) {
        this.history.splice(i, 1)
      } else if (this.history.length > 32) {
        this.history.splice(31)
      }
      this.history.unshift([id, this.filter, this.compressed])
    },
    /**
     * Compute a unique id for the history entry.
     */
    searchId (filter, sections) {
      if (filter === null) {
        filter = ''
      }
      return `${sections}:${filter.trim().toLowerCase()}`
    },
    /**
     * Add the term to the history.
     */
    add(term) {
      this.history.push(term)
    },
    /**
     * Translate the compressed form of search sections into the options flags.
     * @param {*} s the compressed form
     */
    setOptions(s) {
      if (s) {
        for ( const x of this.opts) {
          this.options[x.id] = s.includes(x.letter)
        }
      } else {
        for ( const x of this.opts) {
          this.options[x.id] = x.value
        }
      }
    }
  }
})