diff --git a/mobile/src/api.ts b/mobile/src/api.ts
index f4675a5..20094d8 100644
--- a/mobile/src/api.ts
+++ b/mobile/src/api.ts
@@ -436,13 +436,14 @@ export class GoonClient {
return this.request(`/scenes/${sceneId}/enrich-duration`, { method: 'POST' });
}
- async enrichSceneThumbnail(sceneId: string): Promise<{
+ async enrichSceneThumbnail(sceneId: string, force = false): Promise<{
scene_id: string;
thumbnail_url: string | null;
tube_used: string | null;
sources_updated: number;
}> {
- return this.request(`/scenes/${sceneId}/enrich-thumbnail`, { method: 'POST' });
+ const qs = force ? '?force=true' : '';
+ return this.request(`/scenes/${sceneId}/enrich-thumbnail${qs}`, { method: 'POST' });
}
async enrichSceneStudio(sceneId: string): Promise<{
diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts
index 3dcfc23..2ef1763 100644
--- a/mobile/src/changelog.ts
+++ b/mobile/src/changelog.ts
@@ -16,6 +16,15 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ id: '2026-06-13',
+ date: 'June 2026',
+ items: [
+ 'Source code link added in Settings — the app is open source (MIT).',
+ 'New: a Refresh thumbnail button on a scene when its preview is broken or stale.',
+ 'Fewer dead links — deleted videos on some sites are now detected automatically.',
+ ],
+ },
{
id: '2026-06-12',
date: 'June 2026',
diff --git a/mobile/src/screens/AppLockSettingsScreen.tsx b/mobile/src/screens/AppLockSettingsScreen.tsx
index 2621703..181c310 100644
--- a/mobile/src/screens/AppLockSettingsScreen.tsx
+++ b/mobile/src/screens/AppLockSettingsScreen.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
+ Linking,
Pressable,
ScrollView,
StyleSheet,
@@ -266,11 +267,26 @@ export function AppLockSettingsScreen() {
{APP_VERSION}
+ Linking.openURL(REPO_URL).catch(() => {})}
+ >
+
+ Source code
+ Open source (MIT) — audit it or self-host
+
+ {REPO_LABEL} ›
+
);
}
+// Publiczne repo OSS (zgłoszenie usera 4c5066b8: "no source code repo linked").
+// Sygnał zaufania dla sideloadowanej apki 18+: audyt kodu / self-host / kontrybucja.
+const REPO_URL = 'https://github.com/goon-foss/goon';
+const REPO_LABEL = 'goon-foss/goon';
+
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: theme.bg },
center: { flex: 1, backgroundColor: theme.bg, alignItems: 'center', justifyContent: 'center' },
@@ -320,6 +336,7 @@ const styles = StyleSheet.create({
},
chipActive: { backgroundColor: theme.accent, borderColor: theme.accent },
versionValue: { color: theme.fg, fontSize: 15, fontWeight: '700', fontVariant: ['tabular-nums'] },
+ linkValue: { color: theme.accent, fontSize: 15, fontWeight: '700' },
chipText: { color: theme.muted, fontSize: 13 },
chipTextActive: { color: '#fff', fontWeight: '700' },
});
diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx
index fb4a0d3..1588765 100644
--- a/mobile/src/screens/SceneDetailScreen.tsx
+++ b/mobile/src/screens/SceneDetailScreen.tsx
@@ -119,6 +119,21 @@ export function SceneDetailScreen() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.id]);
+ // Ręczny refresh miniatury (force) — zgłoszenie d3376a71: zepsuta/stała miniaturka
+ // (np. rotting sxyprn/trafficdeposit). Nadpisuje istniejącą świeżą ze strony tube'a.
+ const refreshThumbMutation = useMutation({
+ mutationFn: () => client.enrichSceneThumbnail(id, true),
+ onSuccess: (out) => {
+ queryClient.invalidateQueries({ queryKey: ['scene', id] });
+ queryClient.invalidateQueries({ queryKey: ['scenes'] });
+ Alert.alert(
+ 'Thumbnail',
+ out.thumbnail_url ? 'Refreshed from the source page.' : 'Could not fetch a fresh thumbnail.',
+ );
+ },
+ onError: (e) => Alert.alert('Thumbnail', e instanceof Error ? e.message : String(e)),
+ });
+
// Auto-enrich tags: search-only scrapery nie pobierają tagów z detail page.
// Pornhat ma `js-ajax-tag` data-setup JSON; xhamster/xvideos/youporn/inne mają
// dedicated tag_extract patterny. SceneDetail wywołuje 1 fetch → upsert do DB.
@@ -223,6 +238,18 @@ export function SceneDetailScreen() {
+ {data.playback_sources.some((s) => s.origin?.startsWith('tube:')) && (
+ refreshThumbMutation.mutate()}
+ disabled={refreshThumbMutation.isPending}
+ >
+
+ {refreshThumbMutation.isPending ? 'Refreshing thumbnail…' : '↻ Refresh thumbnail'}
+
+
+ )}
+
{(data.code || data.director) && (
{data.code ? code · {data.code} : null}
@@ -854,6 +881,17 @@ const styles = StyleSheet.create({
fontSize: 13,
},
tagHint: { color: theme.mutedDim, fontSize: 11, marginTop: 6 },
+ refreshThumbBtn: {
+ alignSelf: 'flex-start',
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: theme.border,
+ backgroundColor: theme.card,
+ marginBottom: 12,
+ },
+ refreshThumbText: { color: theme.muted, fontSize: 13, fontWeight: '600' },
pillSource: {
borderColor: theme.accent,