feat(mobile+web): add open in default browser option + cleaner code

This commit is contained in:
daniel31x13
2025-11-20 06:59:13 -05:00
parent 9af731c7eb
commit e908f9c534
10 changed files with 152 additions and 115 deletions

View File

@@ -84,7 +84,7 @@ export default function DashboardScreen() {
contentInsetAdjustmentBehavior="automatic"
>
{orderedSections.map((sectionData, i) => {
if (!collections || !collections[0]) return <></>;
if (!collections || !collections[0]) return null;
const collection = collections.find(
(c) => c.id === sectionData.collectionId
@@ -92,7 +92,7 @@ export default function DashboardScreen() {
return (
<DashboardSection
key={i}
key={sectionData.id}
sectionData={sectionData}
collection={collection}
collectionLinks={

View File

@@ -14,9 +14,9 @@ import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useEffect, useState } from "react";
import {
AppWindowMac,
Check,
FileText,
Globe,
ExternalLink,
LogOut,
Mail,
Moon,
@@ -24,7 +24,6 @@ import {
Sun,
} from "lucide-react-native";
import useDataStore from "@/store/data";
import { ArchivedFormat } from "@/types/global";
import * as Clipboard from "expo-clipboard";
export default function SettingsScreen() {
@@ -145,26 +144,24 @@ export default function SettingsScreen() {
</View>
<View>
<Text className="mb-4 mx-4 text-neutral">
Default Behavior for Opening Links
</Text>
<Text className="mb-4 mx-4 text-neutral">Preferred Browser</Text>
<View className="bg-base-200 rounded-xl flex-col">
<TouchableOpacity
className="flex-row gap-2 items-center justify-between py-3 px-4"
onPress={() =>
updateData({
preferredFormat: null,
preferredBrowser: "app",
})
}
>
<View className="flex-row items-center gap-2">
<Globe
<AppWindowMac
size={20}
color={rawTheme[colorScheme as ThemeName].neutral}
/>
<Text className="text-base-content">Open original content</Text>
<Text className="text-base-content">In app browser</Text>
</View>
{data.preferredFormat === null ? (
{data.preferredBrowser === "app" ? (
<Check
size={20}
color={rawTheme[colorScheme as ThemeName].primary}
@@ -176,18 +173,20 @@ export default function SettingsScreen() {
className="flex-row gap-2 items-center justify-between py-3 px-4"
onPress={() =>
updateData({
preferredFormat: ArchivedFormat.readability,
preferredBrowser: "system",
})
}
>
<View className="flex-row items-center gap-2">
<FileText
<ExternalLink
size={20}
color={rawTheme[colorScheme as ThemeName].neutral}
/>
<Text className="text-base-content">Open reader view</Text>
<Text className="text-base-content">
System default browser
</Text>
</View>
{data.preferredFormat === ArchivedFormat.readability ? (
{data.preferredBrowser === "system" ? (
<Check
size={20}
color={rawTheme[colorScheme as ThemeName].primary}
@@ -230,7 +229,13 @@ export default function SettingsScreen() {
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
container: Platform.select({
ios: {
flex: 1,
paddingBottom: 83,
},
default: {
flex: 1,
},
}),
});

View File

@@ -21,6 +21,7 @@ import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { CalendarDays, Link } from "lucide-react-native";
import useTmpStore from "@/store/tmp";
import { ArchivedFormat } from "@/types/global";
const CACHE_DIR = FileSystem.documentDirectory + "archivedData/readable/";
const htmlPath = (id: string) => `${CACHE_DIR}link_${id}.html`;
@@ -105,16 +106,10 @@ export default function LinkScreen() {
} finally {
setIsLoading(false);
}
}
// original
else if (link?.id && !format && user && link.url) {
setUrl(link.url);
}
// other formats
else if (link?.id && format) {
} else if (link?.id && format) {
setUrl(`${auth.instance}/api/v1/archives/${link.id}?format=${format}`);
} else if (link?.id) {
setUrl(link.url as string);
}
}
@@ -184,7 +179,10 @@ export default function LinkScreen() {
className={isLoading ? "opacity-0" : "flex-1"}
source={{
uri: url,
headers: format ? { Authorization: `Bearer ${auth.session}` } : {},
headers:
format || link?.type !== "url"
? { Authorization: `Bearer ${auth.session}` }
: {},
}}
onLoadEnd={() => setIsLoading(false)}
/>

View File

@@ -11,6 +11,8 @@ import {
import { decode } from "html-entities";
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
import { ArchivedFormat } from "@linkwarden/types";
import getFormatBasedOnPreference from "@linkwarden/lib/getFormatBasedOnPreference";
import getOriginalFormat from "@linkwarden/lib/getOriginalFormat";
import {
atLeastOneFormatAvailable,
formatAvailable,
@@ -64,13 +66,27 @@ const LinkListing = ({ link, dashboard }: Props) => {
dashboard && "rounded-xl"
)}
onLongPress={() => {}}
onPress={() =>
router.navigate(
data.preferredFormat
? `/links/${link.id}?format=${data.preferredFormat}`
: `/links/${link.id}`
)
}
onPress={() => {
if (user) {
const format = getFormatBasedOnPreference({
link,
preference: user.linksRouteTo,
});
data.preferredBrowser === "app"
? router.navigate(
format !== null
? `/links/${link.id}?format=${format}`
: `/links/${link.id}`
)
: Linking.openURL(
format !== null
? auth.instance +
`/preserved/${link?.id}?format=${format}`
: (link.url as string)
);
}
}}
android_ripple={{
color: colorScheme === "dark" ? "rgba(255,255,255,0.2)" : "#ddd",
borderless: false,
@@ -158,20 +174,30 @@ const LinkListing = ({ link, dashboard }: Props) => {
<ContextMenu.Content avoidCollisions>
<ContextMenu.Item
key="open-in-app"
onSelect={() => router.navigate(`/links/${link.id}`)}
>
<ContextMenu.ItemTitle>Open Link</ContextMenu.ItemTitle>
</ContextMenu.Item>
key="open-original"
onSelect={() => {
if (user && link) {
const format = getOriginalFormat(link);
data.preferredBrowser === "app"
? router.navigate(
format !== null
? `/links/${link.id}?format=${format}`
: `/links/${link.id}`
)
: Linking.openURL(
format !== null
? auth.instance +
`/preserved/${link?.id}?format=${format}`
: (link.url as string)
);
}
}}
>
<ContextMenu.ItemTitle>Open Original</ContextMenu.ItemTitle>
</ContextMenu.Item>
{link?.url && (
<>
<ContextMenu.Item
key="open-in-browser"
onSelect={() => Linking.openURL(link.url as string)}
>
<ContextMenu.ItemTitle>Open in Browser</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item
key="copy-url"
onSelect={async () => {

View File

@@ -16,7 +16,7 @@ const useDataStore = create<DataStore>((set, get) => ({
url: "",
},
theme: "system",
preferredFormat: null,
preferredBrowser: "app",
},
setData: async () => {
const dataString = JSON.parse((await AsyncStorage.getItem("data")) || "{}");

View File

@@ -1,8 +1,5 @@
import {
AccountSettings,
LinkIncludingShortenedCollectionAndTags,
} from "@linkwarden/types";
import { generateLinkHref } from "@linkwarden/lib/generateLinkHref";
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
import getFormatBasedOnPreference from "@linkwarden/lib/getFormatBasedOnPreference";
import { LinksRouteTo } from "@linkwarden/prisma/client";
const openLink = (
@@ -13,7 +10,17 @@ const openLink = (
if (user.linksRouteTo === LinksRouteTo.DETAILS) {
openModal();
} else {
window.open(generateLinkHref(link, user), "_blank");
const format = getFormatBasedOnPreference({
link,
preference: user.linksRouteTo,
});
window.open(
format !== null
? `/preserved/${link?.id}?format=${format}`
: (link.url as string),
"_blank"
);
}
};

View File

@@ -1,60 +0,0 @@
import {
AccountSettings,
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@linkwarden/types";
import { LinksRouteTo } from "@linkwarden/prisma/client";
import { formatAvailable } from "@linkwarden/lib/formatStats";
export const generateLinkHref = (
link: LinkIncludingShortenedCollectionAndTags,
account: AccountSettings,
instanceURL: string | null = "",
apiEndpoint: boolean = false
): string => {
// Return the links href based on the account's preference
// If the user's preference is not available, return the original link
let endpoint = "/preserved";
if (apiEndpoint) {
endpoint = "/api/v1/archives";
}
if (account.linksRouteTo === LinksRouteTo.ORIGINAL && link.type === "url") {
return link.url || "";
} else if (account.linksRouteTo === LinksRouteTo.PDF || link.type === "pdf") {
if (!formatAvailable(link, "pdf")) return link.url || "";
return instanceURL + `${endpoint}/${link?.id}?format=${ArchivedFormat.pdf}`;
} else if (
account.linksRouteTo === LinksRouteTo.READABLE &&
link.type === "url"
) {
if (!formatAvailable(link, "readable")) return link.url || "";
return (
instanceURL +
`${endpoint}/${link?.id}?format=${ArchivedFormat.readability}`
);
} else if (
account.linksRouteTo === LinksRouteTo.SCREENSHOT ||
link.type === "image"
) {
if (!formatAvailable(link, "image")) return link.url || "";
return (
instanceURL +
`${endpoint}/${link?.id}?format=${
link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg
}`
);
} else if (account.linksRouteTo === LinksRouteTo.MONOLITH) {
if (!formatAvailable(link, "monolith")) return link.url || "";
return (
instanceURL + `${endpoint}/${link?.id}?format=${ArchivedFormat.monolith}`
);
} else {
return link.url || "";
}
};

View File

@@ -0,0 +1,43 @@
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@linkwarden/types";
import { LinksRouteTo } from "@linkwarden/prisma/client";
import { formatAvailable } from "@linkwarden/lib/formatStats";
const getFormatBasedOnPreference = ({
link,
preference,
}: {
link: LinkIncludingShortenedCollectionAndTags;
preference: LinksRouteTo;
}) => {
// Return the format based on the account's preference
// If the user's preference is not available, return null (original url)
if (preference === LinksRouteTo.ORIGINAL && link.type === "url") {
return null;
} else if (preference === LinksRouteTo.PDF || link.type === "pdf") {
if (!formatAvailable(link, "pdf")) return null;
return ArchivedFormat.pdf;
} else if (preference === LinksRouteTo.READABLE && link.type === "url") {
if (!formatAvailable(link, "readable")) return null;
return ArchivedFormat.readability;
} else if (preference === LinksRouteTo.SCREENSHOT || link.type === "image") {
if (!formatAvailable(link, "image")) return null;
return link?.image?.endsWith("png")
? ArchivedFormat.png
: ArchivedFormat.jpeg;
} else if (preference === LinksRouteTo.MONOLITH) {
if (!formatAvailable(link, "monolith")) return null;
return ArchivedFormat.monolith;
} else {
return null;
}
};
export default getFormatBasedOnPreference;

View File

@@ -0,0 +1,18 @@
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@linkwarden/types";
const getOriginalFormat = (
link: LinkIncludingShortenedCollectionAndTags
): ArchivedFormat | string | null => {
if (link.url && link.type === "url") return link.url;
else if (link.type === "pdf") return ArchivedFormat.pdf;
else if (link.type === "image")
return link.image?.endsWith("png")
? ArchivedFormat.png
: ArchivedFormat.jpeg;
else return null;
};
export default getOriginalFormat;

View File

@@ -186,5 +186,5 @@ export interface MobileData {
url: string;
};
theme: "light" | "dark" | "system";
preferredFormat: ArchivedFormat | null;
preferredBrowser: "app" | "system";
}