feat(web,ui): built-in codeberg.org gitea host and provider detectUrls

This commit is contained in:
2026-08-17 09:56:59 +00:00
parent a30c7bce0a
commit 697925ee0d
22 changed files with 292 additions and 189 deletions
@@ -167,26 +167,30 @@ export const GitPage: React.FC<GitPageProps> = (props) => {
title={t('settings.page.git.title')}
showSaveStatus
>
<div className="flex h-8 min-w-0">
<SortableTabsStrip
items={providerTabs}
activeId={activeProviderTab}
onSelect={(tabId) => setActiveProviderTab(tabId as GitProviderTabId)}
layoutMode="fit"
variant="active-pill"
activePillButtonClassName="h-7"
/>
</div>
<section className="overflow-hidden rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-muted)]">
<div className="border-b border-[var(--surface-subtle)] px-3 py-2">
<div className="flex h-8 min-w-0">
<SortableTabsStrip
items={providerTabs}
activeId={activeProviderTab}
onSelect={(tabId) => setActiveProviderTab(tabId as GitProviderTabId)}
layoutMode="fit"
variant="active-pill"
activePillButtonClassName="h-7"
/>
</div>
</div>
<div role="tabpanel" aria-label={t('settings.git.tabs.github')} hidden={activeProviderTab !== 'github'}>
<GitHubSettings />
</div>
<div role="tabpanel" aria-label={t('settings.git.tabs.gitlab')} hidden={activeProviderTab !== 'gitlab'}>
<GitLabSettings />
</div>
<div role="tabpanel" aria-label={t('settings.git.tabs.gitea')} hidden={activeProviderTab !== 'gitea'}>
<GiteaSettings />
</div>
<div role="tabpanel" aria-label={t('settings.git.tabs.github')} hidden={activeProviderTab !== 'github'}>
<GitHubSettings embedded />
</div>
<div role="tabpanel" aria-label={t('settings.git.tabs.gitlab')} hidden={activeProviderTab !== 'gitlab'}>
<GitLabSettings embedded />
</div>
<div role="tabpanel" aria-label={t('settings.git.tabs.gitea')} hidden={activeProviderTab !== 'gitea'}>
<GiteaSettings embedded />
</div>
</section>
<SettingsSection
title={t('settings.gitIdentities.page.section.title')}
@@ -36,7 +36,7 @@ type DeviceFlowCompleteResponse =
| { connected: true; user: GitHubUser; scope?: string }
| { connected: false; status?: string; error?: string };
export const GitHubSettings: React.FC = () => {
export const GitHubSettings: React.FC<{ embedded?: boolean }> = ({ embedded = false }) => {
const { t } = useI18n();
const { isMobile } = useDeviceInfo();
const runtimeGitHub = getRegisteredRuntimeAPIs()?.github;
@@ -271,14 +271,8 @@ export const GitHubSettings: React.FC = () => {
? t('settings.github.page.accountSource.cli')
: t('settings.github.page.accountSource.oauth');
return (
const accountContent = (
<>
<SettingsSection
title={t('settings.github.page.oauth.title')}
divider={false}
settingsItem="git.github-account"
info={t('settings.github.page.tooltip.connectAccount')}
>
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden flex flex-col">
{connected ? (
<div className={cn("px-4 py-3", isMobile ? "flex flex-col gap-3" : "flex items-center justify-between gap-4")}>
@@ -447,65 +441,95 @@ export const GitHubSettings: React.FC = () => {
</div>
)}
<div className="flex flex-col gap-4 pt-4">
<div className={cn('flex flex-col gap-4', embedded ? 'border-t border-[var(--surface-subtle)] pt-4' : 'pt-4')}>
<ProviderApiBaseUrlInput provider="github" />
<ProviderDetectUrlsInput provider="github" />
</div>
</>
);
</SettingsSection>
{ghCli?.available && !ghCli?.active && (!ghCli.user || ghCli.disabled) && (
<SettingsSection title={t('settings.github.page.ghCli.title')}>
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden">
<div className={cn("px-4 py-3", isMobile ? "flex flex-col gap-3" : "flex items-center justify-between gap-4")}>
<div className={cn("flex min-w-0 items-center gap-4", isMobile ? "w-full" : undefined)}>
{ghCli.user?.avatarUrl ? (
<img
src={ghCli.user.avatarUrl}
alt={ghCli.user.login ? t('settings.github.page.avatarAlt.withLogin', { login: ghCli.user.login }) : t('settings.github.page.avatarAlt.fallback')}
className="h-10 w-10 shrink-0 rounded-full border border-[var(--interactive-border)] bg-[var(--surface-muted)] object-cover"
loading="lazy"
referrerPolicy="no-referrer"
/>
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-[var(--interactive-border)] bg-[var(--surface-muted)]">
<Icon name="github-fill" className="h-4 w-4 text-muted-foreground" />
</div>
)}
<div className="min-w-0 flex-1">
{!ghCli.disabled && ghCli.user && (
<div className="typography-ui-label text-foreground truncate">
{ghCli.user.name?.trim() || ghCli.user.login || 'GitHub'}
</div>
)}
{!ghCli.disabled && ghCli.user?.login && (
<div className={cn("flex items-center gap-2 typography-meta text-muted-foreground mt-0.5", isMobile ? "flex-wrap" : "truncate")}>
<Icon name="github-fill" className="h-3.5 w-3.5 shrink-0" />
<span className="font-mono">{ghCli.user.login}</span>
{ghCli.user.email && <span className="opacity-50"></span>}
{ghCli.user.email && <span>{ghCli.user.email}</span>}
</div>
)}
<div className={cn("typography-meta text-muted-foreground", ghCli.disabled ? "opacity-60" : undefined)}>
{ghCli.disabled
? t('settings.github.page.ghCli.disabledDescription')
: t('settings.github.page.ghCli.fallbackDescription')}
</div>
</div>
const ghCliContent = ghCli?.available && !ghCli?.active && (!ghCli.user || ghCli.disabled) ? (
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden">
<div className={cn("px-4 py-3", isMobile ? "flex flex-col gap-3" : "flex items-center justify-between gap-4")}>
<div className={cn("flex min-w-0 items-center gap-4", isMobile ? "w-full" : undefined)}>
{ghCli.user?.avatarUrl ? (
<img
src={ghCli.user.avatarUrl}
alt={ghCli.user.login ? t('settings.github.page.avatarAlt.withLogin', { login: ghCli.user.login }) : t('settings.github.page.avatarAlt.fallback')}
className="h-10 w-10 shrink-0 rounded-full border border-[var(--interactive-border)] bg-[var(--surface-muted)] object-cover"
loading="lazy"
referrerPolicy="no-referrer"
/>
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-[var(--interactive-border)] bg-[var(--surface-muted)]">
<Icon name="github-fill" className="h-4 w-4 text-muted-foreground" />
</div>
)}
<div className="min-w-0 flex-1">
{!ghCli.disabled && ghCli.user && (
<div className="typography-ui-label text-foreground truncate">
{ghCli.user.name?.trim() || ghCli.user.login || 'GitHub'}
</div>
<Button
size="sm"
variant="outline"
onClick={() => toggleGhCli(!ghCli.disabled)}
disabled={isBusy}
className={cn(isMobile ? "w-full" : undefined)}
>
{ghCli.disabled
? t('settings.github.page.ghCli.actions.enable')
: t('settings.github.page.ghCli.actions.disable')}
</Button>
)}
{!ghCli.disabled && ghCli.user?.login && (
<div className={cn("flex items-center gap-2 typography-meta text-muted-foreground mt-0.5", isMobile ? "flex-wrap" : "truncate")}>
<Icon name="github-fill" className="h-3.5 w-3.5 shrink-0" />
<span className="font-mono">{ghCli.user.login}</span>
{ghCli.user.email && <span className="opacity-50"></span>}
{ghCli.user.email && <span>{ghCli.user.email}</span>}
</div>
)}
<div className={cn("typography-meta text-muted-foreground", ghCli.disabled ? "opacity-60" : undefined)}>
{ghCli.disabled
? t('settings.github.page.ghCli.disabledDescription')
: t('settings.github.page.ghCli.fallbackDescription')}
</div>
</div>
</div>
<Button
size="sm"
variant="outline"
onClick={() => toggleGhCli(!ghCli.disabled)}
disabled={isBusy}
className={cn(isMobile ? "w-full" : undefined)}
>
{ghCli.disabled
? t('settings.github.page.ghCli.actions.enable')
: t('settings.github.page.ghCli.actions.disable')}
</Button>
</div>
</div>
) : null;
if (embedded) {
return (
<>
<div data-settings-item="git.github-account" className="p-4">
{accountContent}
</div>
{ghCliContent && (
<div className="border-t border-[var(--surface-subtle)] px-4 py-4">
{ghCliContent}
</div>
)}
</>
);
}
return (
<>
<SettingsSection
title={t('settings.github.page.oauth.title')}
divider={false}
settingsItem="git.github-account"
info={t('settings.github.page.tooltip.connectAccount')}
>
{accountContent}
</SettingsSection>
{ghCliContent && (
<SettingsSection title={t('settings.github.page.ghCli.title')}>
{ghCliContent}
</SettingsSection>
)}
</>
@@ -24,7 +24,7 @@ const getBaseUrlHost = (baseUrl?: string | null): string => {
}
};
export const GitLabSettings: React.FC = () => {
export const GitLabSettings: React.FC<{ embedded?: boolean }> = ({ embedded = false }) => {
const { t } = useI18n();
const { isMobile } = useDeviceInfo();
const runtimeGitLab = getRegisteredRuntimeAPIs()?.gitlab;
@@ -154,13 +154,8 @@ export const GitLabSettings: React.FC = () => {
const currentAccount = accounts.find((account) => account.current) ?? (accounts.length > 0 ? accounts[0] : null);
const currentBaseUrlHost = getBaseUrlHost(currentAccount?.baseUrl ?? status?.defaultBaseUrl);
return (
<SettingsSection
title={t('settings.gitlab.page.title')}
description={t('settings.gitlab.page.description')}
info={t('settings.gitlab.page.tooltip.connectAccount')}
settingsItem="git.gitlab-account"
>
const sectionContent = (
<>
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden flex flex-col">
{connected ? (
<div className={cn('px-4 py-3', isMobile ? 'flex flex-col gap-3' : 'flex items-center justify-between gap-4')}>
@@ -296,10 +291,29 @@ export const GitLabSettings: React.FC = () => {
)}
</div>
<div className="flex flex-col gap-4 pt-4">
<div className={cn('flex flex-col gap-4', embedded ? 'border-t border-[var(--surface-subtle)] pt-4' : 'pt-4')}>
<ProviderApiBaseUrlInput provider="gitlab" />
<ProviderDetectUrlsInput provider="gitlab" />
</div>
</>
);
if (embedded) {
return (
<div data-settings-item="git.gitlab-account" className="p-4">
{sectionContent}
</div>
);
}
return (
<SettingsSection
title={t('settings.gitlab.page.title')}
description={t('settings.gitlab.page.description')}
info={t('settings.gitlab.page.tooltip.connectAccount')}
settingsItem="git.gitlab-account"
>
{sectionContent}
</SettingsSection>
);
};
@@ -24,7 +24,7 @@ const getBaseUrlHost = (baseUrl?: string | null): string => {
}
};
export const GiteaSettings: React.FC = () => {
export const GiteaSettings: React.FC<{ embedded?: boolean }> = ({ embedded = false }) => {
const { t } = useI18n();
const { isMobile } = useDeviceInfo();
const runtimeGitea = getRegisteredRuntimeAPIs()?.gitea;
@@ -159,13 +159,8 @@ export const GiteaSettings: React.FC = () => {
const currentAccount = accounts.find((account) => account.current) ?? (accounts.length > 0 ? accounts[0] : null);
const currentBaseUrlHost = getBaseUrlHost(currentAccount?.baseUrl);
return (
<SettingsSection
title={t('settings.gitea.page.title')}
description={t('settings.gitea.page.description')}
info={t('settings.gitea.page.tooltip.connectAccount')}
settingsItem="git.gitea-account"
>
const sectionContent = (
<>
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden flex flex-col">
{connected ? (
<div className={cn('px-4 py-3', isMobile ? 'flex flex-col gap-3' : 'flex items-center justify-between gap-4')}>
@@ -306,10 +301,29 @@ export const GiteaSettings: React.FC = () => {
)}
</div>
<div className="flex flex-col gap-4 pt-4">
<div className={cn('flex flex-col gap-4', embedded ? 'border-t border-[var(--surface-subtle)] pt-4' : 'pt-4')}>
<ProviderApiBaseUrlInput provider="gitea" />
<ProviderDetectUrlsInput provider="gitea" />
</div>
</>
);
if (embedded) {
return (
<div data-settings-item="git.gitea-account" className="p-4">
{sectionContent}
</div>
);
}
return (
<SettingsSection
title={t('settings.gitea.page.title')}
description={t('settings.gitea.page.description')}
info={t('settings.gitea.page.tooltip.connectAccount')}
settingsItem="git.gitea-account"
>
{sectionContent}
</SettingsSection>
);
};