Compare commits
20 commits
main
...
for-upstre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f28eba295c | ||
|
|
a7385a58df | ||
|
|
2cf998542f | ||
|
|
b6e5cdb318 | ||
|
|
6ae7b9a3b1 | ||
|
|
bb7b480e6b | ||
|
|
efb6eb1801 | ||
|
|
df891b938c | ||
|
|
366936db8c | ||
|
|
c848cec45b | ||
|
|
b520419d43 | ||
|
|
1e446bf8df | ||
|
|
48880cb8a2 | ||
|
|
d2cef996fd | ||
|
|
b8453d7cfd | ||
|
|
5ef88bd5e8 | ||
|
|
baca849042 | ||
|
|
d8b0d5971a | ||
|
|
be54a06e31 | ||
|
|
ec51cbedcb |
15 changed files with 504 additions and 31 deletions
54
.github/workflows/host-bundle.yml
vendored
Normal file
54
.github/workflows/host-bundle.yml
vendored
Normal 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
5
CONTRIBUTORS.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Contributors
|
||||
|
||||
Thanks to everyone who has helped make Revenge Next better!
|
||||
|
||||
- Rosie (ressurexitcalcator)
|
||||
|
|
@ -29,9 +29,18 @@ export let RootNavigationRef: RootNavigationRef = proxify(
|
|||
2,
|
||||
),
|
||||
]),
|
||||
// TODO: Decouple?
|
||||
ImportTrackerModuleId,
|
||||
]),
|
||||
]).or(
|
||||
withDependencies([
|
||||
loose([
|
||||
relative.withDependencies([], 1),
|
||||
relative.withDependencies(
|
||||
loose([relative(1), relative(2)]),
|
||||
2,
|
||||
),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.keyAs('revenge.discord.modules.mainTabsV2.RootNavigationRef'),
|
||||
)
|
||||
|
|
|
|||
26
lib/externals/src/shopify.ts
vendored
26
lib/externals/src/shopify.ts
vendored
|
|
@ -25,7 +25,6 @@ export let FlashList: typeof import('@shopify/flash-list') = proxify(
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
// TODO: Decouple?
|
||||
ImportTrackerModuleId,
|
||||
]).or(
|
||||
// TODO: Remove when stable > 340206+
|
||||
|
|
@ -38,9 +37,32 @@ export let FlashList: typeof import('@shopify/flash-list') = proxify(
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
// TODO: Decouple?
|
||||
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'),
|
||||
|
|
|
|||
|
|
@ -309,8 +309,7 @@ registerJSMethod(
|
|||
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
meta.nativeErrors = Object.freeze(errors)
|
||||
// TODO: Do we need an errored event specifically?
|
||||
// Nudge open UI to re-render the errors row
|
||||
pEmitter.emit('errored', plugin, errors)
|
||||
pEmitter.emit('flagUpdate', plugin)
|
||||
},
|
||||
)
|
||||
|
|
@ -577,9 +576,7 @@ export function isPluginStartable(plugin: AnyPlugin): boolean {
|
|||
*/
|
||||
export async function handlePluginError(e: unknown, plugin: AnyPlugin) {
|
||||
;(plugin.errors as unknown[]).push(e)
|
||||
|
||||
// TODO: Emit errored event so UI can update?
|
||||
// Update: errored event removed (but status changes are still emitted), so UI should update fine
|
||||
pEmitter.emit('errored', plugin, e)
|
||||
|
||||
nativeLoggingHook(
|
||||
`\u001b[31mPlugin "${plugin.manifest.id}" encountered an error: ${
|
||||
|
|
|
|||
|
|
@ -160,6 +160,8 @@ export interface RepoUpdate {
|
|||
installed: string
|
||||
available: 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 =>
|
||||
listUpdates(repo.url).then(
|
||||
result => {
|
||||
updates.push(...result)
|
||||
updates.push(...result.map(u => ({ ...u, repo: repo.url })))
|
||||
},
|
||||
error => {
|
||||
errors.push({ url: repo.url, error })
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ export interface BrowseFilterAndSortActionSheetProps {
|
|||
/** Checked repository URLs. */
|
||||
checked: string[]
|
||||
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
|
||||
setSort: (sort: BrowseSortKey) => void
|
||||
reverse: boolean
|
||||
|
|
@ -42,6 +47,9 @@ export default function BrowseFilterAndSortActionSheet({
|
|||
repos,
|
||||
checked,
|
||||
setChecked,
|
||||
channels,
|
||||
channel,
|
||||
setChannel,
|
||||
sort,
|
||||
setSort,
|
||||
reverse,
|
||||
|
|
@ -76,6 +84,19 @@ export default function BrowseFilterAndSortActionSheet({
|
|||
)
|
||||
})}
|
||||
</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
|
||||
title="Sort by"
|
||||
defaultValue={sort}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -22,7 +22,14 @@ import {
|
|||
runPluginLate,
|
||||
stopPlugin,
|
||||
} 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 { formatVersion } from '@revenge-mod/plugins/utils'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
|
|
@ -34,7 +41,7 @@ import {
|
|||
showPluginClearDataConfirmation,
|
||||
showPluginUninstallConfirmation,
|
||||
} from '../utils/alerts'
|
||||
import { messageOf, showErrorToast } from '../utils/repos'
|
||||
import { confirmPlan, messageOf, showErrorToast } from '../utils/repos'
|
||||
import { InstalledPluginSwitch, PluginInfo } from './PluginCard'
|
||||
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
|
||||
import {
|
||||
|
|
@ -117,7 +124,7 @@ function StatusSection({ plugin }: { plugin: AnyPlugin }) {
|
|||
<TableRowGroup title="Status">
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="CircleInformationIcon" />}
|
||||
label="Status (TODO)"
|
||||
label="Status"
|
||||
subLabel={bitFieldToString(PluginStatus, status)}
|
||||
/>
|
||||
{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 }) {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
const dependents = getPluginDependents(plugin, true)
|
||||
|
|
@ -152,6 +238,7 @@ function AdvancedSection({ plugin }: { plugin: AnyPlugin }) {
|
|||
|
||||
return (
|
||||
<TableRowGroup title="Advanced">
|
||||
<UpdateRow plugin={plugin} />
|
||||
<IdRow id={id} />
|
||||
<RepositoryRow
|
||||
text={repositoryText}
|
||||
|
|
@ -175,15 +262,37 @@ function AdvancedSection({ plugin }: { plugin: AnyPlugin }) {
|
|||
{dependencies.length > 0 && (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="ListBulletsIcon" />}
|
||||
label="Dependencies (TODO)"
|
||||
label="Dependencies"
|
||||
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 && (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="ListBulletsIcon" />}
|
||||
label="Dependents (TODO)"
|
||||
label="Dependents"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -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" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,18 +1,22 @@
|
|||
import { ToastActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
|
||||
import { JsonStorageUpdateMode } from '@revenge-mod/json-storage'
|
||||
import { reloadApp } from '@revenge-mod/modules/native/app'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
listAllUpdates,
|
||||
refreshAllRepos,
|
||||
updateAllPlugins,
|
||||
} from '@revenge-mod/plugins/_/repositories'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import pluginSettings from '../settings'
|
||||
import { showPluginUpdatesFoundAlert } from './utils/alerts'
|
||||
import { addDefaultRepoIfNeeded } from './repos'
|
||||
import { showErrorToast } from './utils/repos'
|
||||
import type { JsonStorage } from '@revenge-mod/json-storage'
|
||||
import type { PluginApi } from '@revenge-mod/plugins/types'
|
||||
|
||||
|
|
@ -78,12 +82,34 @@ function autoUpdateService(settings: Storage) {
|
|||
setTimeout(async () => {
|
||||
try {
|
||||
const { errors } = await refreshAllRepos()
|
||||
await updateAllPlugins()
|
||||
|
||||
if (!errors.length)
|
||||
await api.jsonStorage.set({
|
||||
lastUpdateCheck: Date.now(),
|
||||
if (errors.length) {
|
||||
ToastActionCreators.open({
|
||||
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) {
|
||||
ToastActionCreators.open({
|
||||
key: 'PLUGIN_UPDATE_CHECK_FAILED',
|
||||
|
|
@ -96,7 +122,6 @@ function autoUpdateService(settings: Storage) {
|
|||
}, AUTO_UPDATE_CHECK_DELAY)
|
||||
}
|
||||
|
||||
// TODO: Add UI for this?
|
||||
function defaultRepoRestoreService(
|
||||
settings: Storage,
|
||||
storage: JsonStorage<Storage>,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ScrollView, View } from 'react-native'
|
||||
import { api } from '..'
|
||||
import { toConfig } from '../repos'
|
||||
import { addDefaultRepoIfNeeded, toConfig } from '../repos'
|
||||
import { formatBytes, messageOf, showErrorToast } from '../utils/repos'
|
||||
import { showRemoveRepoConfirmation } from '../utils/alerts'
|
||||
import type {
|
||||
DownloadProgressEvent,
|
||||
Repo,
|
||||
|
|
@ -101,7 +102,7 @@ function UserRepoRow({
|
|||
label: 'Delete',
|
||||
IconComponent: TrashIconComponent,
|
||||
variant: 'destructive' as const,
|
||||
action: () => onRemove(repo),
|
||||
action: () => showRemoveRepoConfirmation(repo, () => onRemove(repo)),
|
||||
},
|
||||
],
|
||||
]
|
||||
|
|
@ -212,7 +213,6 @@ export default function RevengePluginsAdvancedSettingScreen() {
|
|||
|
||||
const removeRepo = useCallback(
|
||||
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)))
|
||||
|
||||
|
|
@ -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>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ export default function RevengePluginsBrowseSettingScreen() {
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: Let the user pick a release channel
|
||||
/** The channel a listing displays: `latest`, else its first channel. */
|
||||
function displayChannelOf(listing: RepoPluginListing): string | undefined {
|
||||
/** The channel a listing displays: preferred, `latest`, else its first channel. */
|
||||
function displayChannelOf(listing: RepoPluginListing, preferredChannel: string): string | undefined {
|
||||
if (preferredChannel && listing.channels[preferredChannel]) return preferredChannel
|
||||
if (listing.channels.latest) return 'latest'
|
||||
return Object.keys(listing.channels)[0]
|
||||
}
|
||||
|
|
@ -82,15 +82,18 @@ function Screen() {
|
|||
const [excluded, setExcluded] = useState<string[]>([])
|
||||
const [sort, setSort] = useState<BrowseSortKey>('name')
|
||||
const [reverse, setReverse] = useState(false)
|
||||
const [channel, setChannel] = useState('')
|
||||
const [channels, setChannels] = useState<string[]>([])
|
||||
|
||||
const hasFilter = useMemo(
|
||||
() => excluded.length > 0 || sort !== 'name' || reverse,
|
||||
[excluded, sort, reverse],
|
||||
() => excluded.length > 0 || sort !== 'name' || reverse || channel !== '',
|
||||
[excluded, sort, reverse, channel],
|
||||
)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const repos = await listRepos()
|
||||
const all: BrowseEntry[] = []
|
||||
const allChannels = new Set<string>()
|
||||
|
||||
setInternalRepos(
|
||||
repos.filter(repo => repo.internal).map(repo => repo.url),
|
||||
|
|
@ -108,7 +111,9 @@ function Screen() {
|
|||
|
||||
for (const listing of listings) {
|
||||
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
|
||||
? (listing.channels[displayChannel] ?? '')
|
||||
: ''
|
||||
|
|
@ -119,7 +124,6 @@ function Screen() {
|
|||
repoName: repo.name ?? null,
|
||||
repositoryText,
|
||||
version: displayVersion,
|
||||
// TODO: Let the user pick a release channel
|
||||
channel: displayChannel,
|
||||
size:
|
||||
listing.versions[displayVersion]
|
||||
|
|
@ -139,8 +143,9 @@ function Screen() {
|
|||
),
|
||||
)
|
||||
|
||||
setChannels([...allChannels].sort())
|
||||
setEntries(all)
|
||||
}, [])
|
||||
}, [channel, setChannels])
|
||||
|
||||
useEffect(() => {
|
||||
// Show cached indexes right away, then refresh everything
|
||||
|
|
@ -234,6 +239,9 @@ function Screen() {
|
|||
.map(repo => repo.url)
|
||||
.filter(url => !urls.includes(url)),
|
||||
),
|
||||
channels,
|
||||
channel,
|
||||
setChannel,
|
||||
sort,
|
||||
setSort,
|
||||
reverse,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import PluginHasDependentsAlert from '../components/PluginHasDependentsAlert'
|
|||
import PluginMissingDependenciesAlert from '../components/PluginMissingDependenciesAlert'
|
||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||
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/_'
|
||||
|
||||
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(
|
||||
plugin: AnyPlugin,
|
||||
dependents: AnyPlugin[],
|
||||
|
|
@ -116,3 +135,13 @@ export function showPluginHasDependentsAlert(
|
|||
</PluginStatesProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
export function showPluginUpdatesFoundAlert(
|
||||
updates: RepoUpdate[],
|
||||
action: () => Promise<void>,
|
||||
) {
|
||||
AlertActionCreators.openAlert(
|
||||
'plugin-updates-found',
|
||||
<PluginUpdatesFoundAlert updates={updates} action={action} />,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue