The Android app updates itself from our Gitea releases; feed cards fade in instead of bouncing
- API: GET /app/android/latest reads the latest Gitea release (ANDROID_RELEASES_URL, optional token for a private repo) with the APK's SHA-256 from SHA256SUMS; GET /app/android/apk/:name streams that APK. - App (modules/updater): downloads in the background, checks the SHA-256, and installs through PackageInstaller: quietly once when leaving the app (Android 12+), otherwise from the feed banner or Settings > App version. F-Droid builds leave it out and drop the install permissions. - CI: the release notes come from the fastlane changelog; the app shows the part above ---. - Settings: Create-account and Link Spotify buttons for Last.fm and ListenBrainz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,10 +26,11 @@ import { api } from '@/lib/api';
|
||||
import { WEB_URL } from '@/lib/config';
|
||||
import { showError } from '@/lib/errors';
|
||||
import { AppleMusicPermissionError, appleMusicAvailable, connectAppleMusic } from '@/lib/appleMusic';
|
||||
import { SignInError, openSpotifyLinkPage, signInWithService } from '@/lib/connect';
|
||||
import { SignInError, openSignupPage, openSpotifyLinkPage, signInWithService } from '@/lib/connect';
|
||||
import { openReaderSettings, useReader } from '@/lib/nowPlayingReader';
|
||||
import { NTFY_URL, disablePush, enablePush, pushAvailable, usePushStatus } from '@/lib/push';
|
||||
import { useSession } from '@/lib/session';
|
||||
import { checkForUpdate, currentVersion, installUpdate, updaterAvailable, useUpdateState } from '@/lib/updates';
|
||||
import { colors, radius, space } from '@/lib/theme';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
@@ -44,6 +45,7 @@ export default function SettingsScreen() {
|
||||
<ThisPhone />
|
||||
<Connections />
|
||||
<FeaturedPicker />
|
||||
<AppVersion />
|
||||
<Account />
|
||||
</Screen>
|
||||
);
|
||||
@@ -289,6 +291,7 @@ function Connections() {
|
||||
});
|
||||
|
||||
const linkSpotifyOnLastfm = () => openSpotifyLinkPage('lastfm').catch((e) => showError(e));
|
||||
const linkSpotifyOnListenBrainz = () => openSpotifyLinkPage('listenbrainz').catch((e) => showError(e));
|
||||
const spotifyLinked = byProvider.has('lastfm') || byProvider.has('listenbrainz');
|
||||
|
||||
const disconnect = (provider: NowPlayingProvider) =>
|
||||
@@ -382,8 +385,10 @@ function Connections() {
|
||||
<Button small variant="primary" title="Sign in" onPress={() => connectOAuth('lastfm')} />
|
||||
)}
|
||||
</View>
|
||||
{byProvider.has('lastfm') && (
|
||||
{byProvider.has('lastfm') ? (
|
||||
<Button small title="Link Spotify to Last.fm" onPress={linkSpotifyOnLastfm} style={{ alignSelf: 'flex-start' }} />
|
||||
) : (
|
||||
<Button small title="No account? Create one" onPress={() => openSignupPage('lastfm')} style={{ alignSelf: 'flex-start' }} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -393,6 +398,7 @@ function Connections() {
|
||||
active={byProvider.has('listenbrainz')}
|
||||
onConnect={(username) => run(() => api.connectUsername('listenbrainz', username))}
|
||||
onDisconnect={() => disconnect('listenbrainz')}
|
||||
onLinkSpotify={linkSpotifyOnListenBrainz}
|
||||
/>
|
||||
|
||||
<JellyfinConnection
|
||||
@@ -417,12 +423,14 @@ function UsernameConnection({
|
||||
active,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onLinkSpotify,
|
||||
}: {
|
||||
provider: 'lastfm' | 'listenbrainz';
|
||||
status: React.ReactNode;
|
||||
active: boolean;
|
||||
onConnect: (username: string) => Promise<void>;
|
||||
onDisconnect: () => void;
|
||||
onLinkSpotify: () => void;
|
||||
}) {
|
||||
const [username, setUsername] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -454,6 +462,18 @@ function UsernameConnection({
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{active ? (
|
||||
<Button small title={`Link Spotify to ${SERVICE_LABELS[provider]}`} onPress={onLinkSpotify} style={{ alignSelf: 'flex-start' }} />
|
||||
) : (
|
||||
<>
|
||||
<Button small title="No account? Create one" onPress={() => openSignupPage(provider)} style={{ alignSelf: 'flex-start' }} />
|
||||
{provider === 'listenbrainz' && (
|
||||
<Body muted style={styles.small}>
|
||||
ListenBrainz uses MusicBrainz accounts: create one there, then sign in to ListenBrainz once.
|
||||
</Body>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -514,6 +534,58 @@ function JellyfinConnection({
|
||||
);
|
||||
}
|
||||
|
||||
/** This build's version, and checking for a newer one (our own APKs; F-Droid updates its builds itself). */
|
||||
function AppVersion() {
|
||||
const state = useUpdateState();
|
||||
if (!updaterAvailable) return null;
|
||||
const status = (() => {
|
||||
switch (state.status) {
|
||||
case 'checking':
|
||||
return 'Checking for updates…';
|
||||
case 'upToDate':
|
||||
return 'Up to date.';
|
||||
case 'downloading':
|
||||
return `Downloading ${state.update.version}… ${Math.round(state.progress * 100)}%`;
|
||||
case 'ready':
|
||||
return `Version ${state.update.version} is ready to install.`;
|
||||
case 'installing':
|
||||
return `Installing ${state.update.version}…`;
|
||||
case 'error':
|
||||
return state.message;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
return (
|
||||
<Card>
|
||||
<Heading>App version {currentVersion()}</Heading>
|
||||
{status && (
|
||||
<Body muted style={styles.small}>
|
||||
{status}
|
||||
</Body>
|
||||
)}
|
||||
{state.status === 'ready' || state.status === 'installing' ? (
|
||||
<Button
|
||||
small
|
||||
variant="primary"
|
||||
title="Install update"
|
||||
disabled={state.status === 'installing'}
|
||||
onPress={() => installUpdate(true)}
|
||||
style={{ alignSelf: 'flex-start' }}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
small
|
||||
title="Check for updates"
|
||||
loading={state.status === 'checking' || state.status === 'downloading'}
|
||||
onPress={() => checkForUpdate({ force: true })}
|
||||
style={{ alignSelf: 'flex-start' }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Account() {
|
||||
const { me, setMe, signOut } = useSession();
|
||||
const resend = async () => {
|
||||
|
||||
Reference in New Issue
Block a user