feat(web): Download links and Obtainium link generator on Utilities page and onboarding (#20589)

This commit is contained in:
Nicholas 2025-10-17 07:22:00 -04:00 committed by GitHub
parent 3163afd24a
commit cc1cd299f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 226 additions and 3 deletions

View file

@ -0,0 +1,27 @@
<script lang="ts">
import AppDownloadModal from '$lib/modals/AppDownloadModal.svelte';
import ObtainiumConfigModal from '$lib/modals/ObtainiumConfigModal.svelte';
import { Button, HStack, modalManager } from '@immich/ui';
import { mdiCellphoneArrowDownVariant, mdiLinkEdit } from '@mdi/js';
import { t } from 'svelte-i18n';
</script>
<HStack wrap>
<Button
size="large"
shape="semi-round"
onclick={() => modalManager.show(ObtainiumConfigModal, {})}
leadingIcon={mdiLinkEdit}
>
{$t('obtainium_configurator')}
</Button>
<Button
size="large"
shape="semi-round"
onclick={() => modalManager.show(AppDownloadModal, {})}
leadingIcon={mdiCellphoneArrowDownVariant}
>
{$t('app_download_links')}
</Button>
</HStack>
<p>{$t('mobile_app_download_onboarding_note')}</p>

View file

@ -1,7 +1,15 @@
<script lang="ts">
import { AppRoute } from '$lib/constants';
import { Icon } from '@immich/ui';
import { mdiContentDuplicate, mdiCrosshairsGps, mdiImageSizeSelectLarge } from '@mdi/js';
import AppDownloadModal from '$lib/modals/AppDownloadModal.svelte';
import ObtainiumConfigModal from '$lib/modals/ObtainiumConfigModal.svelte';
import { Icon, modalManager } from '@immich/ui';
import {
mdiCellphoneArrowDownVariant,
mdiContentDuplicate,
mdiCrosshairsGps,
mdiImageSizeSelectLarge,
mdiLinkEdit,
} from '@mdi/js';
import { t } from 'svelte-i18n';
const links = [
@ -21,3 +29,27 @@
</a>
{/each}
</div>
<br />
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
<p class="uppercase text-xs font-medium p-4">{$t('download')}</p>
<button
type="button"
onclick={() => modalManager.show(ObtainiumConfigModal, {})}
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
>
<span>
<Icon icon={mdiLinkEdit} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
</span>
{$t('obtainium_configurator')}
</button>
<button
type="button"
onclick={() => modalManager.show(AppDownloadModal, {})}
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
>
<span>
<Icon icon={mdiCellphoneArrowDownVariant} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
</span>
{$t('app_download_links')}
</button>
</div>

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { appStoreBadge, fdroidBadge, Modal, ModalBody, playStoreBadge } from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
onClose: () => void;
}
let { onClose }: Props = $props();
</script>
<Modal title={$t('app_download_links')} size="large" {onClose}>
<ModalBody>
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-5 text-immich-primary dark:text-immich-dark-primary">
<div>
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="fdroid-link">
F-Droid
</label>
<a href="https://f-droid.org/packages/app.alextran.immich/" target="_blank" id="fdroid-link">
<img class="pt-2 pr-10" alt="Get it on F-Droid" src={fdroidBadge} />
</a>
</div>
<div>
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="play-store-link">
Google Play
</label>
<a
href="https://play.google.com/store/apps/details?id=app.alextran.immich"
target="_blank"
id="play-store-link"
>
<img alt="Get it on Google Play" src={playStoreBadge} />
</a>
</div>
<div>
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="app-store-link">
App Store
</label>
<a href="https://apps.apple.com/us/app/immich/id1613945652" target="_blank" id="app-store-link">
<img class="pt-2 pr-5" alt="Download on the App Store" src={appStoreBadge} width="90%" />
</a>
</div>
</div>
</ModalBody>
</Modal>

View file

@ -0,0 +1,94 @@
<script lang="ts">
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { SettingInputFieldType } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
import { createApiKey, Permission } from '@immich/sdk';
import { Button, Modal, ModalBody, obtainiumBadge } from '@immich/ui';
import { t } from 'svelte-i18n';
let inputUrl = $state(location.origin);
let inputApiKey = $state('');
let archVariant = $state('');
let obtainiumLink = $derived(
`https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22app.alextran.immich%22%2C%22url%22%3A%22${inputUrl}%2Fapi%2Fserver%2Fapk-links%22%2C%22author%22%3A%22Immich%22%2C%22name%22%3A%22Immich%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla%2F5.0%20(Linux%3B%20Android%2010%3B%20K)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F114.0.0.0%20Mobile%20Safari%2F537.36%5C%22%7D%2C%7B%5C%22requestHeader%5C%22%3A%5C%22x-api-key%3A%20${inputApiKey}%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22APKLinkHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%2Fv(%5C%5C%5C%5Cd%2B).(%5C%5C%5C%5Cd%2B).(%5C%5C%5C%5Cd%2B)%2F%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%241.%242.%243%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22app-${archVariant}.apk%24%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D`,
);
const handleCreate = async () => {
try {
const { secret } = await createApiKey({
apiKeyCreateDto: {
name: 'Obtainium',
permissions: [Permission.ServerApkLinks],
},
});
inputApiKey = secret;
} catch (error) {
handleError(error, $t('errors.unable_to_create_api_key'));
}
};
interface Props {
onClose: () => void;
}
let { onClose }: Props = $props();
</script>
<Modal title={$t('obtainium_configurator')} size="large" {onClose}>
<ModalBody>
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-5 text-immich-primary dark:text-immich-dark-primary">
<div>
<label
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm"
for="obtainium-configurator"
>
Obtainium
</label>
<div id="obtainium-configurator">
<form>
<div class="mt-2">
<SettingInputField inputType={SettingInputFieldType.TEXT} label={$t('url')} bind:value={inputUrl} />
</div>
<div class="mt-2">
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('api_key')}
bind:value={inputApiKey}
/>
</div>
<div class="">
<Button shape="round" size="small" onclick={() => handleCreate()}>{$t('new_api_key')}</Button>
</div>
<div class="mt-2">
<SettingSelect
label={$t('app_architecture_variant')}
bind:value={archVariant}
options={[
{ value: 'arm64-v8a-release', text: 'arm64-v8a' },
{ value: 'armeabi-v7a-release', text: 'armeabi-v7a' },
{ value: 'release', text: 'universal' },
{ value: 'x86_64-release', text: 'x86_64' },
]}
/>
</div>
</form>
</div>
</div>
<div class="content-center">
{#if inputUrl && inputApiKey && archVariant}
<a
href={obtainiumLink}
class="underline text-sm immich-form-label"
target="_blank"
rel="noreferrer"
id="obtainium-link"
>
<img class="pt-2 pr-5" alt="Get it on Obtainium" src={obtainiumBadge} />
</a>
{:else}
<p class="immich-form-label pb-2 text-sm" id="obtainium-link">
{$t('obtainium_configurator_instructions')}
</p>
{/if}
</div>
</div>
</ModalBody>
</Modal>

View file

@ -5,6 +5,7 @@
import OnboardingCard from '$lib/components/onboarding-page/onboarding-card.svelte';
import OnboardingHello from '$lib/components/onboarding-page/onboarding-hello.svelte';
import OnboardingLocale from '$lib/components/onboarding-page/onboarding-language.svelte';
import OnboardingMobileApp from '$lib/components/onboarding-page/onboarding-mobile-app.svelte';
import OnboardingServerPrivacy from '$lib/components/onboarding-page/onboarding-server-privacy.svelte';
import OnboardingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte';
import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte';
@ -14,7 +15,14 @@
import { retrieveServerConfig, retrieveSystemConfig, serverConfig } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store';
import { setUserOnboarding, updateAdminOnboarding } from '@immich/sdk';
import { mdiCloudCheckOutline, mdiHarddisk, mdiIncognito, mdiThemeLightDark, mdiTranslate } from '@mdi/js';
import {
mdiCellphoneArrowDownVariant,
mdiCloudCheckOutline,
mdiHarddisk,
mdiIncognito,
mdiThemeLightDark,
mdiTranslate,
} from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -26,6 +34,7 @@
| typeof OnboardingStorageTemplate
| typeof OnboardingServerPrivacy
| typeof OnboardingUserPrivacy
| typeof OnboardingMobileApp
| typeof OnboardingLocale;
role: OnboardingRole;
title?: string;
@ -76,6 +85,13 @@
title: $t('admin.backup_onboarding_title'),
icon: mdiCloudCheckOutline,
},
{
name: 'mobile_app',
component: OnboardingMobileApp,
role: OnboardingRole.USER,
title: $t('mobile_app'),
icon: mdiCellphoneArrowDownVariant, // or you can use mdiCellphone
},
]);
let index = $state(0);