import { getAssetIdByName } from '@revenge-mod/assets'
import { styles } from '@revenge-mod/components/_'
import Page from '@revenge-mod/components/Page'
import SearchInput from '@revenge-mod/components/SearchInput'
import { ActionSheetActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { getInternalPluginMeta, pEmitter, pList } from '@revenge-mod/plugins/_'
import {
listRepoPlugins,
listRepos,
refreshAllRepos,
} from '@revenge-mod/plugins/_/repositories'
import { debounce } from '@revenge-mod/utils/callback'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { View } from 'react-native'
import { ClickOutsideProvider } from 'react-native-click-outside'
import { BrowsePluginMasonryFlashList } from '../components/PluginList'
import PluginStatesProvider from '../components/PluginStateProvider'
import {
EnablePluginTooltipProvider,
EssentialPluginTooltipProvider,
} from '../components/TooltipProvider'
import { runInstallFlow } from '../utils/repos'
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
import type { BrowseSortKey } from '../components/BrowseFilterAndSortActionSheet'
import type { BrowseEntry } from '../components/PluginList'
const { Stack, IconButton, LayerScope } = Design
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
const SearchDebounceTime = 100
export default function RevengePluginsBrowseSettingScreen() {
return (
)
}
// TODO: Let the user pick a release channel
/** The version a listing displays: its `latest` channel, else the first channel. */
function displayVersionOf(listing: RepoPluginListing) {
return listing.channels.latest ?? Object.values(listing.channels)[0] ?? ''
}
function compareNames(a: BrowseEntry, b: BrowseEntry) {
return a.listing.name.localeCompare(b.listing.name, undefined, {
sensitivity: 'base',
})
}
const Sorts: Record number> =
{
name: compareNames,
size: (a, b) => a.size - b.size || compareNames(a, b),
}
function Screen() {
const [entries, setEntries] = useState([])
const [search, setSearch] = useState('')
const debouncedSetSearch = useCallback(
debounce(setSearch, SearchDebounceTime),
[],
)
// Unchecked repository URLs, so repos that appear later default to checked
const [excluded, setExcluded] = useState([])
const [sort, setSort] = useState('name')
const [reverse, setReverse] = useState(false)
const load = useCallback(async () => {
const repos = await listRepos()
const all: BrowseEntry[] = []
await Promise.all(
repos
.filter(repo => !repo.internal && repo.enabled)
.map(repo =>
listRepoPlugins(repo.url).then(
listings => {
const repositoryText = repo.name
? `${repo.name} (${repo.url})`
: repo.url
for (const listing of listings) {
const plugin = pList.get(listing.id)
const displayVersion = displayVersionOf(listing)
all.push({
key: `${repo.url}#${listing.id}`,
listing,
repoUrl: repo.url,
repoName: repo.name ?? null,
repositoryText,
version: displayVersion,
size:
listing.versions[displayVersion]
?.size ?? 0,
installed: plugin
? ([
plugin,
getInternalPluginMeta(plugin),
] as const)
: undefined,
})
}
},
// No cached index yet is normal, ignore
() => {},
),
),
)
setEntries(all)
}, [])
useEffect(() => {
// Show cached indexes right away, then refresh everything
load()
refreshAllRepos().then(load, () => {})
}, [load])
// Fresh installs register live, re-mark entries as installed when they do
useEffect(() => {
const handleUpdate = () => load()
pEmitter.on('register', handleUpdate)
pEmitter.on('unregister', handleUpdate)
return () => {
pEmitter.off('register', handleUpdate)
pEmitter.off('unregister', handleUpdate)
}
}, [load])
const install = useCallback(
async (entry: BrowseEntry) => {
await runInstallFlow(entry.listing.id)
load()
},
[load],
)
// Repositories that currently have entries, for the filter sheet
const repos = useMemo(() => {
const seen = new Map()
for (const entry of entries)
if (!seen.has(entry.repoUrl))
seen.set(entry.repoUrl, entry.repoName)
return [...seen].map(([url, name]) => ({ url, name }))
}, [entries])
const visible = useMemo(() => {
const query = search.toLowerCase()
const result = entries.filter(entry => {
if (excluded.includes(entry.repoUrl)) return false
if (!query) return true
const { name, description, author, id } = entry.listing
return (
name.toLowerCase().includes(query) ||
description.toLowerCase().includes(query) ||
author.toLowerCase().includes(query) ||
id.toLowerCase().includes(query)
)
})
result.sort(Sorts[sort])
if (reverse) result.reverse()
return result
}, [entries, excluded, search, sort, reverse])
return (
<>
ActionSheetActionCreators.openLazy(
import(
'../components/BrowseFilterAndSortActionSheet'
),
'browse-filter-and-sort',
{
repos,
checked: repos
.map(repo => repo.url)
.filter(url => !excluded.includes(url)),
setChecked: (urls: string[]) =>
setExcluded(
repos
.map(repo => repo.url)
.filter(url => !urls.includes(url)),
),
sort,
setSort,
reverse,
setReverse,
},
)
}
/>
>
)
}