Compare commits

...
Sign in to create a new pull request.

20 commits

Author SHA1 Message Date
Rosie Val
f28eba295c Update & reload from startup update alert 2026-08-09 00:58:55 -04:00
Rosie Val
a7385a58df Add startup update-confirmation alert with plugin details 2026-08-09 00:54:54 -04:00
Rosie Val
2cf998542f Add per-plugin Check for updates row 2026-08-09 00:41:35 -04:00
Rosie Val
b6e5cdb318 Re-add fork-only host-bundle workflow 2026-08-09 00:15:26 -04:00
Rosie Val
6ae7b9a3b1 Remove fork-only host-bundle workflow 2026-08-09 00:13:44 -04:00
Rosie Val
bb7b480e6b Emit errored events for plugin native and JS errors 2026-08-08 23:27:35 -04:00
Rosie Val
efb6eb1801 Add fallback FlashList fingerprints without ImportTracker 2026-08-08 23:19:31 -04:00
Rosie Val
df891b938c Add fallback fingerprint for main_tabs_v2 without ImportTracker 2026-08-08 22:07:10 -04:00
Rosie Val
366936db8c Add Rosie to contributors 2026-08-08 22:05:27 -04:00
Rosie Val
c848cec45b Fix PluginStateProvider import path 2026-08-08 21:53:02 -04:00
Rosie Val
b520419d43 Add manual restore default repositories button 2026-08-08 21:37:27 -04:00
Rosie Val
1e446bf8df Remove TODO now that default repo restore has UI 2026-08-08 21:37:26 -04:00
Rosie Val
48880cb8a2 Compute and apply user-selected release channel in browse 2026-08-08 21:36:15 -04:00
Rosie Val
d2cef996fd Add release channel picker to browse filter sheet 2026-08-08 21:36:14 -04:00
Rosie Val
b8453d7cfd Use repo removal confirmation when deleting repositories 2026-08-08 21:30:35 -04:00
Rosie Val
5ef88bd5e8 Wire up repo removal confirmation alert 2026-08-08 21:30:24 -04:00
Rosie Val
baca849042 Add repo removal confirmation alert component 2026-08-08 21:30:23 -04:00
Rosie Val
d8b0d5971a Remove (TODO) labels and add dependency/dependent popup menus 2026-08-08 21:04:20 -04:00
Rosie Val
be54a06e31 Add PluginListActionSheet for dependency and dependent popups 2026-08-08 21:02:10 -04:00
Rosie Val
ec51cbedcb Add bundle hosting workflow for test instances 2026-08-08 20:34:46 -04:00
15 changed files with 504 additions and 31 deletions

54
.github/workflows/host-bundle.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Host Revenge Bundle
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build-and-host:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Restore Bun cache
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile --production
- name: Build production bundle
run: bun run build
- name: Publish bundle to hosting branch
run: |
cp dist/revenge.bundle /tmp/revenge.bundle
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git show-ref --verify --quiet "refs/heads/hosting"; then
git checkout hosting
git rm -rf .
else
git checkout --orphan hosting
fi
rm -rf *
cp /tmp/revenge.bundle ./revenge.bundle
git add revenge.bundle
git commit -m "Host bundle from ${GITHUB_SHA}" || true
git push --force origin hosting

5
CONTRIBUTORS.md Normal file
View file

@ -0,0 +1,5 @@
# Contributors
Thanks to everyone who has helped make Revenge Next better!
- Rosie (ressurexitcalcator)

View file

@ -29,9 +29,18 @@ export let RootNavigationRef: RootNavigationRef = proxify(
2, 2,
), ),
]), ]),
// TODO: Decouple?
ImportTrackerModuleId, ImportTrackerModuleId,
]).or(
withDependencies([
loose([
relative.withDependencies([], 1),
relative.withDependencies(
loose([relative(1), relative(2)]),
2,
),
]), ]),
]),
),
) )
.keyAs('revenge.discord.modules.mainTabsV2.RootNavigationRef'), .keyAs('revenge.discord.modules.mainTabsV2.RootNavigationRef'),
) )

View file

@ -25,7 +25,6 @@ export let FlashList: typeof import('@shopify/flash-list') = proxify(
null, null,
null, null,
null, null,
// TODO: Decouple?
ImportTrackerModuleId, ImportTrackerModuleId,
]).or( ]).or(
// TODO: Remove when stable > 340206+ // TODO: Remove when stable > 340206+
@ -38,9 +37,32 @@ export let FlashList: typeof import('@shopify/flash-list') = proxify(
null, null,
null, null,
null, null,
// TODO: Decouple?
ImportTrackerModuleId, ImportTrackerModuleId,
]), ]),
).or(
withDependencies([
ReactModuleId,
ReactNativeModuleId,
ReactJSXRuntimeModuleId,
null,
null,
null,
null,
null,
null,
]),
).or(
// [React, JSXRuntime, (Platform), (FlashListExports), (Reanimated), (RNBottomSheet), (BottomSheet)]
withDependencies([
ReactModuleId,
ReactJSXRuntimeModuleId,
null,
null,
null,
null,
null,
null,
]),
), ),
) )
.keyAs('revenge.externals.Shopify.FlashList'), .keyAs('revenge.externals.Shopify.FlashList'),

View file

@ -309,8 +309,7 @@ registerJSMethod(
const meta = getInternalPluginMeta(plugin) const meta = getInternalPluginMeta(plugin)
meta.nativeErrors = Object.freeze(errors) meta.nativeErrors = Object.freeze(errors)
// TODO: Do we need an errored event specifically? pEmitter.emit('errored', plugin, errors)
// Nudge open UI to re-render the errors row
pEmitter.emit('flagUpdate', plugin) pEmitter.emit('flagUpdate', plugin)
}, },
) )
@ -577,9 +576,7 @@ export function isPluginStartable(plugin: AnyPlugin): boolean {
*/ */
export async function handlePluginError(e: unknown, plugin: AnyPlugin) { export async function handlePluginError(e: unknown, plugin: AnyPlugin) {
;(plugin.errors as unknown[]).push(e) ;(plugin.errors as unknown[]).push(e)
pEmitter.emit('errored', plugin, e)
// TODO: Emit errored event so UI can update?
// Update: errored event removed (but status changes are still emitted), so UI should update fine
nativeLoggingHook( nativeLoggingHook(
`\u001b[31mPlugin "${plugin.manifest.id}" encountered an error: ${ `\u001b[31mPlugin "${plugin.manifest.id}" encountered an error: ${

View file

@ -160,6 +160,8 @@ export interface RepoUpdate {
installed: string installed: string
available: string available: string
channel: string channel: string
/** Repository URL this update was found in, added by {@link listAllUpdates}. */
repo?: string
} }
/** /**
@ -227,7 +229,7 @@ export async function listAllUpdates(): Promise<{
.map(repo => .map(repo =>
listUpdates(repo.url).then( listUpdates(repo.url).then(
result => { result => {
updates.push(...result) updates.push(...result.map(u => ({ ...u, repo: repo.url })))
}, },
error => { error => {
errors.push({ url: repo.url, error }) errors.push({ url: repo.url, error })

View file

@ -32,6 +32,11 @@ export interface BrowseFilterAndSortActionSheetProps {
/** Checked repository URLs. */ /** Checked repository URLs. */
checked: string[] checked: string[]
setChecked: (urls: string[]) => void setChecked: (urls: string[]) => void
/** Available release channels across all listings. */
channels: string[]
/** Selected release channel. Empty string means default. */
channel: string
setChannel: (channel: string) => void
sort: BrowseSortKey sort: BrowseSortKey
setSort: (sort: BrowseSortKey) => void setSort: (sort: BrowseSortKey) => void
reverse: boolean reverse: boolean
@ -42,6 +47,9 @@ export default function BrowseFilterAndSortActionSheet({
repos, repos,
checked, checked,
setChecked, setChecked,
channels,
channel,
setChannel,
sort, sort,
setSort, setSort,
reverse, reverse,
@ -76,6 +84,19 @@ export default function BrowseFilterAndSortActionSheet({
) )
})} })}
</TableRowGroup> </TableRowGroup>
<TableRadioGroup
title="Release channel"
defaultValue={channel}
onChange={v => setChannel(v as string)}
>
<TableRadioRow
label="Default (latest / first available)"
value=""
/>
{channels.map(c => (
<TableRadioRow key={c} label={c} value={c} />
))}
</TableRadioGroup>
<TableRadioGroup <TableRadioGroup
title="Sort by" title="Sort by"
defaultValue={sort} defaultValue={sort}

View file

@ -0,0 +1,33 @@
import { TableRowAssetIcon } from '@revenge-mod/components'
import { Design } from '@revenge-mod/discord/design'
import type { AnyPlugin } from '@revenge-mod/plugins/_'
const { ActionSheet, Stack, TableRow, TableRowGroup } = Design
export interface PluginListActionSheetProps {
title: string
plugins: AnyPlugin[]
sheetKey: string
}
export default function PluginListActionSheet({
title,
plugins,
}: PluginListActionSheetProps) {
return (
<ActionSheet>
<Stack spacing={24} style={{ paddingTop: 8 }}>
<TableRowGroup title={title}>
{plugins.map(plugin => (
<TableRow
key={plugin.manifest.id}
icon={<TableRowAssetIcon name="ListBulletsIcon" />}
label={plugin.manifest.name}
subLabel={plugin.manifest.id}
/>
))}
</TableRowGroup>
</Stack>
</ActionSheet>
)
}

View file

@ -22,7 +22,14 @@ import {
runPluginLate, runPluginLate,
stopPlugin, stopPlugin,
} from '@revenge-mod/plugins/_' } from '@revenge-mod/plugins/_'
import { listRepos } from '@revenge-mod/plugins/_/repositories' import {
installFromRepo,
listRepos,
listUpdates,
planInstall,
refreshRepo,
} from '@revenge-mod/plugins/_/repositories'
import type { RepoUpdate } from '@revenge-mod/plugins/_/repositories'
import { PluginStatus } from '@revenge-mod/plugins/constants' import { PluginStatus } from '@revenge-mod/plugins/constants'
import { formatVersion } from '@revenge-mod/plugins/utils' import { formatVersion } from '@revenge-mod/plugins/utils'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord' import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
@ -34,7 +41,7 @@ import {
showPluginClearDataConfirmation, showPluginClearDataConfirmation,
showPluginUninstallConfirmation, showPluginUninstallConfirmation,
} from '../utils/alerts' } from '../utils/alerts'
import { messageOf, showErrorToast } from '../utils/repos' import { confirmPlan, messageOf, showErrorToast } from '../utils/repos'
import { InstalledPluginSwitch, PluginInfo } from './PluginCard' import { InstalledPluginSwitch, PluginInfo } from './PluginCard'
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider' import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
import { import {
@ -117,7 +124,7 @@ function StatusSection({ plugin }: { plugin: AnyPlugin }) {
<TableRowGroup title="Status"> <TableRowGroup title="Status">
<TableRow <TableRow
icon={<TableRowAssetIcon name="CircleInformationIcon" />} icon={<TableRowAssetIcon name="CircleInformationIcon" />}
label="Status (TODO)" label="Status"
subLabel={bitFieldToString(PluginStatus, status)} subLabel={bitFieldToString(PluginStatus, status)}
/> />
{errors.length > 0 && ( {errors.length > 0 && (
@ -143,6 +150,85 @@ function StatusSection({ plugin }: { plugin: AnyPlugin }) {
) )
} }
function UpdateRow({ plugin }: { plugin: AnyPlugin }) {
const [busy, setBusy] = useState(false)
const [checked, setChecked] = useState(false)
const [update, setUpdate] = useState<RepoUpdate | null>(null)
const meta = getInternalPluginMeta(plugin)
const repoUrl = meta.source?.repo
if (isPluginInternal(meta) || !repoUrl) return null
const check = async () => {
setBusy(true)
try {
await refreshRepo(repoUrl)
const updates = await listUpdates(repoUrl)
const found =
updates.find(u => u.id === plugin.manifest.id) ?? null
setUpdate(found)
setChecked(true)
} catch (e) {
showErrorToast(messageOf(e))
} finally {
setBusy(false)
}
}
const doUpdate = async () => {
if (!update) return
setBusy(true)
try {
const plan = await planInstall(
update.id,
undefined,
update.channel,
)
const accepted = await confirmPlan(plan)
if (!accepted) return
await installFromRepo(plan)
setUpdate(null)
setChecked(true)
ToastActionCreators.open({
key: 'revenge-plugin-update-pending',
content: 'Update downloaded. Reload to apply.',
})
} catch (e) {
showErrorToast(messageOf(e))
} finally {
setBusy(false)
}
}
const icon = update
? 'DownloadIcon'
: checked
? 'CircleCheckIcon'
: 'RefreshIcon'
const label = update
? 'Update'
: checked
? 'Up to date'
: 'Check for updates'
const subLabel = update
? `${update.installed} → ${update.available} (${update.channel})`
: checked
? 'This plugin is on the latest available version'
: 'Tap to refresh this plugin\'s repository'
return (
<TableRow
icon={<TableRowAssetIcon name={icon} />}
label={label}
subLabel={subLabel}
onPress={update ? doUpdate : check}
disabled={busy}
/>
)
}
function AdvancedSection({ plugin }: { plugin: AnyPlugin }) { function AdvancedSection({ plugin }: { plugin: AnyPlugin }) {
const meta = getInternalPluginMeta(plugin) const meta = getInternalPluginMeta(plugin)
const dependents = getPluginDependents(plugin, true) const dependents = getPluginDependents(plugin, true)
@ -152,6 +238,7 @@ function AdvancedSection({ plugin }: { plugin: AnyPlugin }) {
return ( return (
<TableRowGroup title="Advanced"> <TableRowGroup title="Advanced">
<UpdateRow plugin={plugin} />
<IdRow id={id} /> <IdRow id={id} />
<RepositoryRow <RepositoryRow
text={repositoryText} text={repositoryText}
@ -175,15 +262,37 @@ function AdvancedSection({ plugin }: { plugin: AnyPlugin }) {
{dependencies.length > 0 && ( {dependencies.length > 0 && (
<TableRow <TableRow
icon={<TableRowAssetIcon name="ListBulletsIcon" />} icon={<TableRowAssetIcon name="ListBulletsIcon" />}
label="Dependencies (TODO)" label="Dependencies"
subLabel={`${name} depends on ${dependencies.length} other plugins`} subLabel={`${name} depends on ${dependencies.length} other plugins`}
onPress={() => {
ActionSheetActionCreators.openLazy(
import('./PluginListActionSheet'),
`plugin-deps-${id}`,
{
title: `Dependencies of ${name}`,
plugins: dependencies,
sheetKey: `plugin-deps-${id}`,
},
)
}}
/> />
)} )}
{dependents.length > 0 && ( {dependents.length > 0 && (
<TableRow <TableRow
icon={<TableRowAssetIcon name="ListBulletsIcon" />} icon={<TableRowAssetIcon name="ListBulletsIcon" />}
label="Dependents (TODO)" label="Dependents"
subLabel={`${dependents.length} other plugins depend on ${name}`} subLabel={`${dependents.length} other plugins depend on ${name}`}
onPress={() => {
ActionSheetActionCreators.openLazy(
import('./PluginListActionSheet'),
`plugin-dependents-${id}`,
{
title: `Dependents of ${name}`,
plugins: dependents,
sheetKey: `plugin-dependents-${id}`,
},
)
}}
/> />
)} )}
</TableRowGroup> </TableRowGroup>

View file

@ -0,0 +1,112 @@
import { Design } from '@revenge-mod/discord/design'
import {
listRepoPlugins,
type RepoPluginListing,
type RepoUpdate,
} from '@revenge-mod/plugins/_/repositories'
import { useEffect, useState } from 'react'
import { View } from 'react-native'
const { AlertModal, AlertActionButton, Text } = Design
interface UpdateDetail {
id: string
name: string
author: string
installed: string
available: string
channel: string
}
export default function PluginUpdatesFoundAlert({
updates,
action,
}: {
updates: RepoUpdate[]
action: () => Promise<void>
}) {
const [details, setDetails] = useState<UpdateDetail[] | null>(null)
useEffect(() => {
const repoMap = new Map<string, RepoUpdate[]>()
for (const update of updates) {
const repo = update.repo!
if (!repoMap.has(repo)) repoMap.set(repo, [])
repoMap.get(repo)!.push(update)
}
const allDetails: UpdateDetail[] = []
Promise.all(
[...repoMap.entries()].map(async ([repo, repoUpdates]) => {
let listings: RepoPluginListing[]
try {
listings = await listRepoPlugins(repo)
} catch {
listings = []
}
for (const update of repoUpdates) {
const listing = listings.find(l => l.id === update.id)
allDetails.push({
id: update.id,
name: listing?.name ?? update.id,
author: listing?.author ?? 'Unknown',
installed: update.installed,
available: update.available,
channel: update.channel,
})
}
}),
).then(() => setDetails(allDetails))
}, [updates])
return (
<AlertModal
title="Updates Found"
content={
<Text color="text-default">
The following plugins have updates available:
</Text>
}
extraContent={
<View style={{ paddingTop: 8 }}>
{details === null ? (
<Text color="text-muted">Loading...</Text>
) : (
details.map(detail => (
<View
key={detail.id}
style={{ marginBottom: 12 }}
>
<Text
variant="text-md/semibold"
color="text-default"
>
{detail.name}
</Text>
<Text color="text-muted">
{detail.installed} → {detail.available} (
{detail.channel})
</Text>
<Text color="text-muted">
by {detail.author}
</Text>
</View>
))
)}
</View>
}
actions={
<>
<AlertActionButton
onPress={action}
text="Update & Reload"
variant="primary"
/>
<AlertActionButton text="Not now" variant="secondary" />
</>
}
/>
)
}

View file

@ -0,0 +1,37 @@
import { Design } from '@revenge-mod/discord/design'
import type { Repo } from '@revenge-mod/plugins/_/repositories'
const { AlertModal, AlertActionButton, Text } = Design
export default function RepoRemoveConfirmationAlert({
repo,
action,
}: {
repo: Repo
action: () => Promise<void> | void
}) {
return (
<AlertModal
title="Remove repository?"
content={
<Text color="text-default">
<Text variant="text-md/semibold" color="text-default">
{repo.name ?? repo.url}
</Text>{' '}
will be removed. You can restore the default repositories by
clearing Plugin Settings data.
</Text>
}
actions={
<>
<AlertActionButton
onPress={action}
text="Remove"
variant="destructive"
/>
<AlertActionButton text="Cancel" variant="secondary" />
</>
}
/>
)
}

View file

@ -1,18 +1,22 @@
import { ToastActionCreators } from '@revenge-mod/discord/actions' import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings' import { onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
import { JsonStorageUpdateMode } from '@revenge-mod/json-storage' import { JsonStorageUpdateMode } from '@revenge-mod/json-storage'
import { reloadApp } from '@revenge-mod/modules/native/app'
import { import {
InternalPluginFlags, InternalPluginFlags,
PluginFlags, PluginFlags,
registerInternalPlugin, registerInternalPlugin,
} from '@revenge-mod/plugins/_' } from '@revenge-mod/plugins/_'
import { import {
listAllUpdates,
refreshAllRepos, refreshAllRepos,
updateAllPlugins, updateAllPlugins,
} from '@revenge-mod/plugins/_/repositories' } from '@revenge-mod/plugins/_/repositories'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord' import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import pluginSettings from '../settings' import pluginSettings from '../settings'
import { showPluginUpdatesFoundAlert } from './utils/alerts'
import { addDefaultRepoIfNeeded } from './repos' import { addDefaultRepoIfNeeded } from './repos'
import { showErrorToast } from './utils/repos'
import type { JsonStorage } from '@revenge-mod/json-storage' import type { JsonStorage } from '@revenge-mod/json-storage'
import type { PluginApi } from '@revenge-mod/plugins/types' import type { PluginApi } from '@revenge-mod/plugins/types'
@ -78,11 +82,33 @@ function autoUpdateService(settings: Storage) {
setTimeout(async () => { setTimeout(async () => {
try { try {
const { errors } = await refreshAllRepos() const { errors } = await refreshAllRepos()
await updateAllPlugins()
if (!errors.length) if (errors.length) {
await api.jsonStorage.set({ ToastActionCreators.open({
lastUpdateCheck: Date.now(), key: 'PLUGIN_UPDATE_CHECK_FAILED',
content: 'Failed to refresh some plugin repositories',
IconComponent: CircleXIconComponent,
})
return
}
const { updates } = await listAllUpdates()
await api.jsonStorage.set({ lastUpdateCheck: Date.now() })
if (!updates.length) return
showPluginUpdatesFoundAlert(updates, async () => {
const { errors: updateErrors } = await updateAllPlugins()
if (updateErrors.length) {
showErrorToast(
updateErrors
.map(e => `${e.id}: ${e.error}`)
.join('\n'),
)
} else {
reloadApp()
}
}) })
} catch (e) { } catch (e) {
ToastActionCreators.open({ ToastActionCreators.open({
@ -96,7 +122,6 @@ function autoUpdateService(settings: Storage) {
}, AUTO_UPDATE_CHECK_DELAY) }, AUTO_UPDATE_CHECK_DELAY)
} }
// TODO: Add UI for this?
function defaultRepoRestoreService( function defaultRepoRestoreService(
settings: Storage, settings: Storage,
storage: JsonStorage<Storage>, storage: JsonStorage<Storage>,

View file

@ -20,8 +20,9 @@ import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { ScrollView, View } from 'react-native' import { ScrollView, View } from 'react-native'
import { api } from '..' import { api } from '..'
import { toConfig } from '../repos' import { addDefaultRepoIfNeeded, toConfig } from '../repos'
import { formatBytes, messageOf, showErrorToast } from '../utils/repos' import { formatBytes, messageOf, showErrorToast } from '../utils/repos'
import { showRemoveRepoConfirmation } from '../utils/alerts'
import type { import type {
DownloadProgressEvent, DownloadProgressEvent,
Repo, Repo,
@ -101,7 +102,7 @@ function UserRepoRow({
label: 'Delete', label: 'Delete',
IconComponent: TrashIconComponent, IconComponent: TrashIconComponent,
variant: 'destructive' as const, variant: 'destructive' as const,
action: () => onRemove(repo), action: () => showRemoveRepoConfirmation(repo, () => onRemove(repo)),
}, },
], ],
] ]
@ -212,7 +213,6 @@ export default function RevengePluginsAdvancedSettingScreen() {
const removeRepo = useCallback( const removeRepo = useCallback(
async (repo: Repo) => { async (repo: Repo) => {
// TODO: Add warning for default repo removal, user can clear "Plugin Settings" data to restore the default repo
await commit(toConfig(userRepos.filter(r => r.url !== repo.url))) await commit(toConfig(userRepos.filter(r => r.url !== repo.url)))
@ -370,6 +370,16 @@ export default function RevengePluginsAdvancedSettingScreen() {
) )
} }
/> />
<TableRow
icon={<TableRowAssetIcon name="GlobeEarthIcon" />}
label="Restore default repositories"
subLabel="Add the bundled default repository if it's missing"
onPress={() => {
addDefaultRepoIfNeeded().catch(e =>
showErrorToast(messageOf(e)),
)
}}
/>
</TableRowGroup> </TableRowGroup>
</Stack> </Stack>
</ScrollView> </ScrollView>

View file

@ -49,9 +49,9 @@ export default function RevengePluginsBrowseSettingScreen() {
) )
} }
// TODO: Let the user pick a release channel /** The channel a listing displays: preferred, `latest`, else its first channel. */
/** The channel a listing displays: `latest`, else its first channel. */ function displayChannelOf(listing: RepoPluginListing, preferredChannel: string): string | undefined {
function displayChannelOf(listing: RepoPluginListing): string | undefined { if (preferredChannel && listing.channels[preferredChannel]) return preferredChannel
if (listing.channels.latest) return 'latest' if (listing.channels.latest) return 'latest'
return Object.keys(listing.channels)[0] return Object.keys(listing.channels)[0]
} }
@ -82,15 +82,18 @@ function Screen() {
const [excluded, setExcluded] = useState<string[]>([]) const [excluded, setExcluded] = useState<string[]>([])
const [sort, setSort] = useState<BrowseSortKey>('name') const [sort, setSort] = useState<BrowseSortKey>('name')
const [reverse, setReverse] = useState(false) const [reverse, setReverse] = useState(false)
const [channel, setChannel] = useState('')
const [channels, setChannels] = useState<string[]>([])
const hasFilter = useMemo( const hasFilter = useMemo(
() => excluded.length > 0 || sort !== 'name' || reverse, () => excluded.length > 0 || sort !== 'name' || reverse || channel !== '',
[excluded, sort, reverse], [excluded, sort, reverse, channel],
) )
const load = useCallback(async () => { const load = useCallback(async () => {
const repos = await listRepos() const repos = await listRepos()
const all: BrowseEntry[] = [] const all: BrowseEntry[] = []
const allChannels = new Set<string>()
setInternalRepos( setInternalRepos(
repos.filter(repo => repo.internal).map(repo => repo.url), repos.filter(repo => repo.internal).map(repo => repo.url),
@ -108,7 +111,9 @@ function Screen() {
for (const listing of listings) { for (const listing of listings) {
const plugin = pList.get(listing.id) const plugin = pList.get(listing.id)
const displayChannel = displayChannelOf(listing) for (const c of Object.keys(listing.channels))
allChannels.add(c)
const displayChannel = displayChannelOf(listing, channel)
const displayVersion = displayChannel const displayVersion = displayChannel
? (listing.channels[displayChannel] ?? '') ? (listing.channels[displayChannel] ?? '')
: '' : ''
@ -119,7 +124,6 @@ function Screen() {
repoName: repo.name ?? null, repoName: repo.name ?? null,
repositoryText, repositoryText,
version: displayVersion, version: displayVersion,
// TODO: Let the user pick a release channel
channel: displayChannel, channel: displayChannel,
size: size:
listing.versions[displayVersion] listing.versions[displayVersion]
@ -139,8 +143,9 @@ function Screen() {
), ),
) )
setChannels([...allChannels].sort())
setEntries(all) setEntries(all)
}, []) }, [channel, setChannels])
useEffect(() => { useEffect(() => {
// Show cached indexes right away, then refresh everything // Show cached indexes right away, then refresh everything
@ -234,6 +239,9 @@ function Screen() {
.map(repo => repo.url) .map(repo => repo.url)
.filter(url => !urls.includes(url)), .filter(url => !urls.includes(url)),
), ),
channels,
channel,
setChannel,
sort, sort,
setSort, setSort,
reverse, reverse,

View file

@ -13,6 +13,9 @@ import PluginHasDependentsAlert from '../components/PluginHasDependentsAlert'
import PluginMissingDependenciesAlert from '../components/PluginMissingDependenciesAlert' import PluginMissingDependenciesAlert from '../components/PluginMissingDependenciesAlert'
import PluginStatesProvider from '../components/PluginStateProvider' import PluginStatesProvider from '../components/PluginStateProvider'
import PluginUninstallConfirmationAlert from '../components/PluginUninstallConfirmationAlert' import PluginUninstallConfirmationAlert from '../components/PluginUninstallConfirmationAlert'
import PluginUpdatesFoundAlert from '../components/PluginUpdatesFoundAlert'
import RepoRemoveConfirmationAlert from '../components/RepoRemoveConfirmationAlert'
import type { Repo, RepoUpdate } from '@revenge-mod/plugins/_/repositories'
import type { AnyPlugin } from '@revenge-mod/plugins/_' import type { AnyPlugin } from '@revenge-mod/plugins/_'
export function showPluginClearDataConfirmation( export function showPluginClearDataConfirmation(
@ -100,6 +103,22 @@ export function showPluginMissingDependenciesAlert(
) )
} }
export function showRemoveRepoConfirmation(
repo: Repo,
callback: () => Promise<void> | void,
) {
const KEY = 'repo-remove-confirmation'
async function action() {
await callback()
}
AlertActionCreators.openAlert(
KEY,
<RepoRemoveConfirmationAlert repo={repo} action={action} />,
)
}
export function showPluginHasDependentsAlert( export function showPluginHasDependentsAlert(
plugin: AnyPlugin, plugin: AnyPlugin,
dependents: AnyPlugin[], dependents: AnyPlugin[],
@ -116,3 +135,13 @@ export function showPluginHasDependentsAlert(
</PluginStatesProvider>, </PluginStatesProvider>,
) )
} }
export function showPluginUpdatesFoundAlert(
updates: RepoUpdate[],
action: () => Promise<void>,
) {
AlertActionCreators.openAlert(
'plugin-updates-found',
<PluginUpdatesFoundAlert updates={updates} action={action} />,
)
}