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,
|
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'),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
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,
|
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'),
|
||||||
|
|
|
||||||
|
|
@ -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: ${
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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 { 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>,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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} />,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue