Compare commits

...

68 Commits

Author SHA1 Message Date
Daniel
463883383b Merge pull request #1455 from linkwarden/dev
v2.13.1
2025-10-15 15:25:59 +03:30
daniel31x13
f13ab8500a bump version 2025-10-15 07:03:11 -04:00
Daniel
3c7bcfe3e4 Merge pull request #1451 from supercoolspy/fix/singlefile-title
Fix/singlefile title
2025-10-15 14:29:38 +03:30
daniel31x13
476a9d78a4 bug fixed 2025-10-15 06:58:14 -04:00
Daniel
fa2d439b3e Merge pull request #1448 from supercoolspy/fix/singlefile-view
fix: support using monolith content for other archive formats
2025-10-15 14:21:03 +03:30
daniel31x13
0907c3caa2 bug fix 2025-10-15 06:49:45 -04:00
Daniel
a4266b1a62 New Crowdin updates (#1441)
* New translations common.json (Spanish)

* New translations common.json (Spanish)

* New translations common.json (Chinese Simplified)

* New translations common.json (Chinese Simplified)

---------

Co-authored-by: LinkwardenBot <bot@linkwarden.app>
2025-10-14 19:41:48 -04:00
daniel31x13
05bebd8703 swap from base64 images to url for emails 2025-10-14 19:11:45 -04:00
daniel31x13
2c727ccd47 improved email templates 2025-10-14 19:03:49 -04:00
daniel31x13
1205cdce1c remove unused import 2025-10-13 17:29:04 -04:00
daniel31x13
6ae4c37d0c minor fix 2025-10-13 17:28:35 -04:00
daniel31x13
4148c0f5fb add trial ended email for cloud 2025-10-13 17:26:09 -04:00
daniel31x13
f7e7fda779 feat(mobile): proper deployment 2025-10-13 10:56:29 -04:00
Spy
fbafa3df4e fix: get title from HTML with monolith 2025-10-11 16:11:13 -07:00
Spy
7d45249e8f fix: support using monolith content for other archives 2025-10-10 22:44:26 -07:00
Daniel
96472243db Merge pull request #1290 from jvanbruegge/fix-http-links
fix INVALID_PROTOCOL when saving http website
2025-10-04 04:08:02 -04:00
Daniel
4ef0e6bd86 Merge pull request #1271 from Tchoupinax/main
fix: add support for password manager for login page
2025-10-04 04:01:39 -04:00
daniel31x13
daf5dc4f22 fix merge conflict 2025-10-04 04:01:14 -04:00
Daniel
43f8da30d3 Merge pull request #1215 from claflico/feat/synology-auth
Add Synology OIDC as login option based upon Authelia settings successful login
2025-10-04 03:56:22 -04:00
daniel31x13
d2e5dbd521 update yarn.lock 2025-10-04 03:54:37 -04:00
Daniel
9c05aaf2df Merge pull request #1214 from tcriess/perplexity
add ai-sdk/perplexity provider
2025-10-04 03:52:06 -04:00
Daniel
4f2e26c31f Merge pull request #1426 from linkwarden/i18n
New Crowdin updates
2025-10-04 03:29:14 -04:00
LinkwardenBot
328e031ebd New translations common.json (Portuguese, Brazilian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
93d4e58306 New translations common.json (Portuguese, Brazilian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
2255fb3a6c New translations common.json (Dutch) 2025-10-03 15:26:53 +00:00
LinkwardenBot
d23a935cae New translations common.json (Ukrainian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
57bde730f8 New translations common.json (Ukrainian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
01e0587012 New translations common.json (Ukrainian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
c9f8b233d5 New translations common.json (Ukrainian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
2685188ba6 New translations common.json (Ukrainian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
942672ea95 New translations common.json (Romanian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
ac57cc0202 New translations common.json (Chinese Traditional) 2025-10-03 15:26:53 +00:00
LinkwardenBot
69b5919a96 New translations common.json (Ukrainian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
09823fe776 New translations common.json (Polish) 2025-10-03 15:26:53 +00:00
LinkwardenBot
a97cb229ff New translations common.json (Dutch) 2025-10-03 15:26:53 +00:00
LinkwardenBot
cbf3756f16 New translations common.json (Japanese) 2025-10-03 15:26:53 +00:00
LinkwardenBot
900f62487b New translations common.json (Italian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
bc6f9a55e4 New translations common.json (German) 2025-10-03 15:26:53 +00:00
LinkwardenBot
e7b7a7f46a New translations common.json (Spanish) 2025-10-03 15:26:53 +00:00
LinkwardenBot
f3b23dadd1 New translations common.json (French) 2025-10-03 15:26:53 +00:00
LinkwardenBot
8de57d3875 New translations common.json (Russian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
bfb6b25c28 New translations common.json (Portuguese, Brazilian) 2025-10-03 15:26:53 +00:00
LinkwardenBot
a24786a513 New translations common.json (Chinese Simplified) 2025-10-03 15:26:52 +00:00
LinkwardenBot
6b9f181585 New translations common.json (Turkish) 2025-10-03 15:26:52 +00:00
Daniel
b21e2c6ffd Merge pull request #1425 from linkwarden/dev
Dev
2025-09-26 14:30:02 -04:00
daniel31x13
96dcbcfb79 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-09-26 14:29:39 -04:00
daniel31x13
9c45f933cd typo 2025-09-26 14:29:38 -04:00
Daniel
427c062075 Merge pull request #1424 from linkwarden/dev
Dev
2025-09-26 14:29:11 -04:00
Daniel
2130422c2a Merge pull request #1418 from linkwarden/i18n
New Crowdin updates
2025-09-26 14:28:41 -04:00
daniel31x13
8133900e76 added subscription banner 2025-09-26 14:27:39 -04:00
Thorsten Riess
9ad4a3ee87 add ai-sdk/perplexity provider 2025-09-26 06:46:47 +02:00
LinkwardenBot
8c6169320a New translations common.json (Romanian) 2025-09-25 13:45:44 +00:00
LinkwardenBot
ea0916d826 New translations common.json (Chinese Traditional) 2025-09-25 13:45:44 +00:00
LinkwardenBot
14550a89e6 New translations common.json (Ukrainian) 2025-09-25 13:45:44 +00:00
LinkwardenBot
c1c5b3e953 New translations common.json (Polish) 2025-09-25 13:45:44 +00:00
LinkwardenBot
bebe9671cb New translations common.json (Dutch) 2025-09-25 13:45:44 +00:00
LinkwardenBot
bcf12e25a1 New translations common.json (Japanese) 2025-09-25 13:45:44 +00:00
LinkwardenBot
4d9725f66c New translations common.json (Italian) 2025-09-25 13:45:44 +00:00
LinkwardenBot
aa045801cc New translations common.json (German) 2025-09-25 13:45:44 +00:00
LinkwardenBot
8dd54a1c26 New translations common.json (Spanish) 2025-09-25 13:45:44 +00:00
LinkwardenBot
ce76eb0b74 New translations common.json (French) 2025-09-25 13:45:44 +00:00
LinkwardenBot
453dfbcfb8 New translations common.json (Russian) 2025-09-25 13:45:43 +00:00
LinkwardenBot
0ca4f72e53 New translations common.json (Portuguese, Brazilian) 2025-09-25 13:45:43 +00:00
LinkwardenBot
645e8dc4b2 New translations common.json (Chinese Simplified) 2025-09-25 13:45:43 +00:00
LinkwardenBot
bab44a942a New translations common.json (Turkish) 2025-09-25 13:45:43 +00:00
Jan van Brügge
327826d760 fix INVALID_PROTOCOL when saving http website
Using a https agent to fetch a http site causes this error:
TypeError [ERR_INVALID_PROTOCOL]: Protocol "http:" not supported. Expected "https:"
2025-07-14 01:34:58 +01:00
Tchoupinax
2441470849 fix: add support for password manager for login page 2025-06-29 11:20:18 +02:00
Cory Claflin
be532d5455 Add Synology OIDC as login option based upon Authelia settings successful login 2025-06-06 22:22:35 -05:00
54 changed files with 2384 additions and 1816 deletions

View File

@@ -68,6 +68,10 @@ ANTHROPIC_MODEL=
OPENROUTER_API_KEY=
OPENROUTER_MODEL=
# https://ai-sdk.dev/providers/ai-sdk-providers/perplexity
PERPLEXITY_API_KEY=
PERPLEXITY_MODEL=
# MeiliSearch Settings
MEILI_HOST=
MEILI_MASTER_KEY=
@@ -399,6 +403,13 @@ STRAVA_CUSTOM_NAME=
STRAVA_CLIENT_ID=
STRAVA_CLIENT_SECRET=
# Synology
NEXT_PUBLIC_SYNOLOGY_ENABLED=
SYNOLOGY_CUSTOM_NAME=
SYNOLOGY_CLIENT_ID=
SYNOLOGY_CLIENT_SECRET=
SYNOLOGY_WELLKNOWN_URL=
# Todoist
NEXT_PUBLIC_TODOIST_ENABLED=
TODOIST_CUSTOM_NAME=

View File

@@ -10,7 +10,16 @@
"newArchEnabled": true,
"ios": {
"supportsTablet": true,
"bundleIdentifier": "app.linkwarden"
"bundleIdentifier": "app.linkwarden",
"entitlements": {
"com.apple.security.application-groups": ["group.app.linkwarden"]
},
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false,
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
}
}
},
"android": {
"adaptiveIcon": {
@@ -41,8 +50,26 @@
}
],
"expo-secure-store",
"expo-share-intent",
"./plugins/with-daynight-transparent-nav"
[
"expo-share-intent",
{
"iosAppGroupIdentifier": "group.app.linkwarden"
}
],
"./plugins/with-daynight-transparent-nav",
[
"expo-build-properties",
{
"android": {
"enableProguardInReleaseBuilds": true,
"extraProguardRules": "-keep public class com.horcrux.svg.** {*;}",
"allowBackup": false,
"compileSdkVersion": 35,
"targetSdkVersion": 35,
"buildToolsVersion": "35.0.0"
}
}
]
],
"experiments": {
"typedRoutes": true
@@ -55,6 +82,15 @@
"androidNavigationBar": {
"backgroundColor": "#ffffff",
"barStyle": "dark-content"
}
},
"extra": {
"router": {
"origin": false
},
"eas": {
"projectId": "34f82639-7a25-4ebe-81c8-2db521b612cf"
}
},
"owner": "linkwarden"
}
}

View File

@@ -8,10 +8,10 @@ import {
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import React, { useEffect, useMemo, useState } from "react";
import { useDashboardData } from "@linkwarden/router/dashboardData";
import useAuthStore from "@/store/auth";
import React, { useEffect, useMemo, useState } from "react";
import { DashboardSection, DashboardSectionType } from "@prisma/client";
import { DashboardSection } from "@linkwarden/prisma/client";
import { useUser } from "@linkwarden/router/user";
import { useCollections } from "@linkwarden/router/collections";
import { useTags } from "@linkwarden/router/tags";
@@ -32,6 +32,13 @@ import {
} from "lucide-react-native";
import Spinner from "@/components/ui/Spinner";
// Don't remove this, spent a couple of days to figure out why the app crashes in production :|
type DashboardSectionType =
| "STATS"
| "RECENT_LINKS"
| "PINNED_LINKS"
| "COLLECTION";
export default function DashboardScreen() {
const { auth } = useAuthStore();
const {
@@ -76,6 +83,12 @@ export default function DashboardScreen() {
});
}, [dashboardSections]);
const RenderItem = React.memo(
({ item }: { item: LinkIncludingShortenedCollectionAndTags }) => {
return <LinkListing link={item} dashboard />;
}
);
interface SectionProps {
sectionData: { type: DashboardSectionType };
collection?: any;
@@ -100,7 +113,7 @@ export default function DashboardScreen() {
collectionLinks = [],
}) => {
switch (sectionData.type) {
case DashboardSectionType.STATS:
case "STATS":
return (
<View className="flex-col gap-4 max-w-full px-5">
<View className="flex-row gap-4">
@@ -134,7 +147,7 @@ export default function DashboardScreen() {
</View>
);
case DashboardSectionType.RECENT_LINKS:
case "RECENT_LINKS":
return (
<>
<View className="flex-row justify-between items-center px-5">
@@ -203,7 +216,7 @@ export default function DashboardScreen() {
</>
);
case DashboardSectionType.PINNED_LINKS:
case "PINNED_LINKS":
return (
<>
<View className="flex-row justify-between items-center px-5">
@@ -265,7 +278,7 @@ export default function DashboardScreen() {
</>
);
case DashboardSectionType.COLLECTION:
case "COLLECTION":
return collection?.id ? (
<>
<View className="flex-row justify-between items-center px-5">
@@ -331,12 +344,6 @@ export default function DashboardScreen() {
}
};
const RenderItem = React.memo(
({ item }: { item: LinkIncludingShortenedCollectionAndTags }) => {
return <LinkListing link={item} dashboard />;
}
);
return (
<SafeAreaView
style={styles.container}

View File

@@ -19,6 +19,7 @@ import useDataStore from "@/store/data";
import useAuthStore from "@/store/auth";
import { QueryClient } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system";
import { KeyboardProvider } from "react-native-keyboard-controller";
const queryClient = new QueryClient({
defaultOptions: {
@@ -116,85 +117,87 @@ export default function RootLayout() {
<View
style={[{ flex: 1 }, colorScheme === "dark" ? darkTheme : lightTheme]}
>
<SheetProvider>
{!isLoading && (
<Stack
screenOptions={{
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-200"],
headerShown: false,
contentStyle: {
backgroundColor:
rawTheme[colorScheme as ThemeName]["base-100"],
},
...Platform.select({
android: {
statusBarStyle: colorScheme === "dark" ? "light" : "dark",
statusBarBackgroundColor:
<KeyboardProvider>
<SheetProvider>
{!isLoading && (
<Stack
screenOptions={{
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-200"],
headerShown: false,
contentStyle: {
backgroundColor:
rawTheme[colorScheme as ThemeName]["base-100"],
},
}),
}}
>
{/* <Stack.Screen name="(tabs)" /> */}
<Stack.Screen
name="links/[id]"
options={{
headerShown: true,
headerBackTitle: "Back",
headerTitle: "",
headerTintColor: colorScheme === "dark" ? "white" : "black",
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-100"],
headerStyle: {
backgroundColor:
colorScheme === "dark"
? rawTheme["dark"]["base-100"]
: "white",
},
}}
/>
<Stack.Screen
name="login"
options={{
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-100"],
...Platform.select({
android: {
statusBarStyle:
colorScheme === "light" ? "light" : "dark",
statusBarStyle: colorScheme === "dark" ? "light" : "dark",
statusBarBackgroundColor:
rawTheme[colorScheme as ThemeName]["primary"],
rawTheme[colorScheme as ThemeName]["base-100"],
},
}),
}}
/>
<Stack.Screen
name="index"
options={{
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-100"],
...Platform.select({
android: {
statusBarStyle:
colorScheme === "light" ? "light" : "dark",
statusBarBackgroundColor:
rawTheme[colorScheme as ThemeName]["primary"],
>
{/* <Stack.Screen name="(tabs)" /> */}
<Stack.Screen
name="links/[id]"
options={{
headerShown: true,
headerBackTitle: "Back",
headerTitle: "",
headerTintColor: colorScheme === "dark" ? "white" : "black",
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-100"],
headerStyle: {
backgroundColor:
colorScheme === "dark"
? rawTheme["dark"]["base-100"]
: "white",
},
}),
}}
/>
<Stack.Screen
name="incoming"
options={{
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-100"],
}}
/>
<Stack.Screen name="+not-found" />
</Stack>
)}
</SheetProvider>
}}
/>
<Stack.Screen
name="login"
options={{
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-100"],
...Platform.select({
android: {
statusBarStyle:
colorScheme === "light" ? "light" : "dark",
statusBarBackgroundColor:
rawTheme[colorScheme as ThemeName]["primary"],
},
}),
}}
/>
<Stack.Screen
name="index"
options={{
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-100"],
...Platform.select({
android: {
statusBarStyle:
colorScheme === "light" ? "light" : "dark",
statusBarBackgroundColor:
rawTheme[colorScheme as ThemeName]["primary"],
},
}),
}}
/>
<Stack.Screen
name="incoming"
options={{
navigationBarColor:
rawTheme[colorScheme as ThemeName]["base-100"],
}}
/>
<Stack.Screen name="+not-found" />
</Stack>
)}
</SheetProvider>
</KeyboardProvider>
</View>
</PersistQueryClientProvider>
);

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useState } from "react";
import {
View,
ActivityIndicator,
@@ -12,7 +12,6 @@ import NetInfo from "@react-native-community/netinfo";
import useAuthStore from "@/store/auth";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useUser } from "@linkwarden/router/user";
import { generateLinkHref } from "@linkwarden/lib/generateLinkHref";
import { useWindowDimensions } from "react-native";
import RenderHtml from "@linkwarden/react-native-render-html";
import ElementNotSupported from "@/components/ElementNotSupported";
@@ -70,6 +69,7 @@ export default function LinkScreen() {
}, [user, link]);
async function fetchLinkData() {
// readable
if (link?.id && format === "3") {
const apiUrl = `${auth.instance}/api/v1/archives/${link.id}?format=${format}`;
setUrl(apiUrl);
@@ -87,11 +87,15 @@ export default function LinkScreen() {
} finally {
setIsLoading(false);
}
} else if (link?.id && !format && user) {
setUrl(
generateLinkHref(link, { ...user, password: "" }, auth.instance, true)
);
} else if (link?.id && format) {
}
// original
else if (link?.id && !format && user && link.url) {
setUrl(link.url);
}
// other formats
else if (link?.id && format) {
setUrl(`${auth.instance}/api/v1/archives/${link.id}?format=${format}`);
}
}

View File

@@ -8,6 +8,10 @@ import { useEffect, useState } from "react";
import { View, Text, Dimensions, TouchableOpacity, Image } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import Svg, { Path } from "react-native-svg";
import {
KeyboardAwareScrollView,
KeyboardToolbar,
} from "react-native-keyboard-controller";
export default function HomeScreen() {
const { auth, signIn } = useAuthStore();
@@ -50,117 +54,134 @@ export default function HomeScreen() {
}
return (
<View className="flex-col justify-end h-full bg-primary relative">
<View className="my-auto">
<Image
source={require("@/assets/images/linkwarden.png")}
className="w-[120px] h-[120px] mx-auto"
/>
</View>
<Text className="text-base-100 text-7xl font-bold ml-8">Login</Text>
<View>
<Text className="text-base-100 text-2xl mx-8 mt-3" numberOfLines={1}>
Login to{" "}
{form.instance === "https://cloud.linkwarden.app"
? "cloud.linkwarden.app"
: form.instance}
</Text>
<TouchableOpacity
onPress={() => {
if (showInstanceField) {
setForm({ ...form, instance: "https://cloud.linkwarden.app" });
}
setShowInstanceField(!showInstanceField);
}}
className="mx-8 mt-1 self-start"
>
<Text className="text-neutral-content text-sm">
{!showInstanceField ? "Change server" : "Use official server"}
</Text>
</TouchableOpacity>
</View>
<Svg
viewBox="0 0 1440 320"
width={Dimensions.get("screen").width}
height={100}
<>
<KeyboardAwareScrollView
bottomOffset={62}
contentContainerClassName="flex-col justify-end h-full bg-base-100 relative"
>
<Path
fill={rawTheme[colorScheme as ThemeName]["base-100"]}
fill-opacity="1"
d="M0,256L48,234.7C96,213,192,171,288,176C384,181,480,235,576,266.7C672,299,768,309,864,277.3C960,245,1056,171,1152,122.7C1248,75,1344,53,1392,42.7L1440,32L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"
/>
</Svg>
<View className="flex-col justify-end h-auto duration-100 pt-10 bg-base-100 -mt-2 pb-10 gap-4 w-full px-4">
{showInstanceField && (
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Instance URL"
selectTextOnFocus={false}
value={form.instance}
onChangeText={(text) => setForm({ ...form, instance: text })}
/>
)}
{method === "password" ? (
<>
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Email or Username"
value={form.user}
onChangeText={(text) => setForm({ ...form, user: text })}
<View className="flex-col justify-end h-full bg-primary relative">
<View className="my-auto">
<Image
source={require("@/assets/images/linkwarden.png")}
className="w-[120px] h-[120px] mx-auto"
/>
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Password"
secureTextEntry
value={form.password}
onChangeText={(text) => setForm({ ...form, password: text })}
</View>
<Text className="text-base-100 text-7xl font-bold ml-8">Login</Text>
<View>
<Text
className="text-base-100 text-2xl mx-8 mt-3"
numberOfLines={1}
>
Login to{" "}
{form.instance === "https://cloud.linkwarden.app"
? "cloud.linkwarden.app"
: form.instance}
</Text>
<TouchableOpacity
onPress={() => {
if (showInstanceField) {
setForm({
...form,
instance: "https://cloud.linkwarden.app",
});
}
setShowInstanceField(!showInstanceField);
}}
className="mx-8 mt-1 self-start"
>
<Text className="text-neutral-content text-sm">
{!showInstanceField ? "Change server" : "Use official server"}
</Text>
</TouchableOpacity>
</View>
<Svg
viewBox="0 0 1440 320"
width={Dimensions.get("screen").width}
height={100}
>
<Path
fill={rawTheme[colorScheme as ThemeName]["base-100"]}
fill-opacity="1"
d="M0,256L48,234.7C96,213,192,171,288,176C384,181,480,235,576,266.7C672,299,768,309,864,277.3C960,245,1056,171,1152,122.7C1248,75,1344,53,1392,42.7L1440,32L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"
/>
</>
) : (
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Access Token"
secureTextEntry
value={form.token}
onChangeText={(text) => setForm({ ...form, token: text })}
/>
)}
</Svg>
<View className="flex-col justify-end h-auto duration-100 pt-10 bg-base-100 -mt-2 pb-10 gap-4 w-full px-4">
{showInstanceField && (
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Instance URL"
selectTextOnFocus={false}
value={form.instance}
onChangeText={(text) => setForm({ ...form, instance: text })}
/>
)}
{method === "password" ? (
<>
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Email or Username"
value={form.user}
onChangeText={(text) => setForm({ ...form, user: text })}
/>
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Password"
secureTextEntry
value={form.password}
onChangeText={(text) => setForm({ ...form, password: text })}
/>
</>
) : (
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Access Token"
secureTextEntry
value={form.token}
onChangeText={(text) => setForm({ ...form, token: text })}
/>
)}
<TouchableOpacity
onPress={() =>
setMethod(method === "password" ? "token" : "password")
}
className="w-fit mx-auto"
>
<Text className="text-primary w-fit text-center">
{method === "password"
? "Login with Access Token"
: "Login with Username/Password"}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() =>
setMethod(method === "password" ? "token" : "password")
}
className="w-fit mx-auto"
>
<Text className="text-primary w-fit text-center">
{method === "password"
? "Login with Access Token"
: "Login with Username/Password"}
</Text>
</TouchableOpacity>
<Button
variant="accent"
size="lg"
onPress={() => {
if (((form.user && form.password) || form.token) && form.instance) {
signIn(form.user, form.password, form.instance, form.token);
}
}}
>
<Text className="text-white text-xl">Login</Text>
</Button>
<TouchableOpacity
className="w-fit mx-auto"
onPress={() => SheetManager.show("support-sheet")}
>
<Text className="text-neutral text-center w-fit">Need help?</Text>
</TouchableOpacity>
</View>
</View>
<Button
variant="accent"
size="lg"
onPress={() => {
if (
((form.user && form.password) || form.token) &&
form.instance
) {
signIn(form.user, form.password, form.instance, form.token);
}
}}
>
<Text className="text-white text-xl">Login</Text>
</Button>
<TouchableOpacity
className="w-fit mx-auto"
onPress={() => SheetManager.show("support-sheet")}
>
<Text className="text-neutral text-center w-fit">Need help?</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAwareScrollView>
<KeyboardToolbar />
</>
);
}

30
apps/mobile/eas.json Normal file
View File

@@ -0,0 +1,30 @@
{
"cli": {
"version": ">= 16.20.1",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"simulator": {
"extends": "development",
"ios": {
"simulator": true
}
},
"production": {
"distribution": "store",
"autoIncrement": true,
"channel": "production"
}
},
"submit": {
"production": {
"ios": {
"ascAppId": "6752550960"
}
}
}
}

View File

@@ -16,6 +16,7 @@
"dependencies": {
"@expo/vector-icons": "^14.0.2",
"@linkwarden/lib": "*",
"@linkwarden/prisma": "*",
"@linkwarden/react-native-render-html": "^6.3.4",
"@linkwarden/router": "*",
"@linkwarden/types": "*",
@@ -30,6 +31,7 @@
"expo": "~52.0.18",
"expo-application": "~6.0.2",
"expo-blur": "~14.0.1",
"expo-build-properties": "~0.13.3",
"expo-clipboard": "~7.0.1",
"expo-constants": "~17.0.3",
"expo-dev-client": "~5.0.6",
@@ -44,6 +46,7 @@
"expo-status-bar": "~2.0.0",
"expo-symbols": "~0.2.0",
"expo-system-ui": "~4.0.6",
"expo-updates": "~0.27.4",
"expo-web-browser": "~14.0.1",
"html-entities": "^2.6.0",
"lucide-react-native": "^0.536.0",
@@ -53,8 +56,9 @@
"react-native": "0.76.9",
"react-native-actions-sheet": "^0.9.7",
"react-native-gesture-handler": "~2.20.2",
"react-native-ios-context-menu": "3.1.0",
"react-native-ios-utilities": "5.1.2",
"react-native-ios-context-menu": "3.1.3",
"react-native-ios-utilities": "5.1.7",
"react-native-keyboard-controller": "^1.19.0",
"react-native-mmkv": "^3.2.0",
"react-native-reanimated": "3.16.2",
"react-native-safe-area-context": "4.12.0",

View File

@@ -24,10 +24,17 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useUser } from "@linkwarden/router/user";
import Link from "next/link";
const STRIPE_ENABLED = process.env.NEXT_PUBLIC_STRIPE === "true";
const TRIAL_PERIOD_DAYS =
Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS) || 14;
export default function Navbar() {
const { t } = useTranslation();
const router = useRouter();
const { data: user } = useUser();
const [sidebar, setSidebar] = useState(false);
@@ -50,87 +57,126 @@ export default function Navbar() {
const [newCollectionModal, setNewCollectionModal] = useState(false);
const [uploadFileModal, setUploadFileModal] = useState(false);
const [daysLeft, setDaysLeft] = useState<number>(0);
const [isTrialing, setIsTrialing] = useState<boolean>(false);
useEffect(() => {
if (user?.createdAt) {
const trialEndTime =
new Date(user.createdAt).getTime() +
(1 + Number(TRIAL_PERIOD_DAYS)) * 86400000; // Add 1 to account for the current day
setDaysLeft(Math.floor((trialEndTime - Date.now()) / 86400000));
}
}, [user]);
useEffect(() => {
const isTrialing =
user?.id &&
!user?.subscription?.active &&
!user.parentSubscription?.active;
setIsTrialing(Boolean(isTrialing));
}, [user, daysLeft]);
return (
<div className="flex justify-between gap-2 items-center pl-3 pr-4 py-2 border-solid border-b-neutral-content border-b">
<Button
variant="ghost"
size="icon"
className="text-neutral lg:hidden sm:inline-flex"
onClick={() => {
setSidebar(true);
document.body.style.overflow = "hidden";
}}
>
<i className="bi-list text-xl leading-none" />
</Button>
<>
{STRIPE_ENABLED && isTrialing && (
<Link
href="/subscribe"
className="w-full text-sm cursor-pointer select-none bg-base-200"
>
<p className="w-full text-center flex items-center justify-center gap-1 underline decoration-dotted underline-offset-4 hover:opacity-70 duration-200 py-1 px-2">
<i className="bi-clock text-primary" />
{daysLeft === 1
? t("trial_left_singular")
: t("trial_left_plural", { count: daysLeft })}
</p>
</Link>
)}
<div className="flex justify-between gap-2 items-center pl-3 pr-4 py-2 border-solid border-b-neutral-content border-b">
<Button
variant="ghost"
size="icon"
className="text-neutral lg:hidden sm:inline-flex"
onClick={() => {
setSidebar(true);
document.body.style.overflow = "hidden";
}}
>
<i className="bi-list text-xl leading-none" />
</Button>
<SearchBar />
<SearchBar />
<div className="flex items-center gap-2">
<ToggleDarkMode hideInMobile />
<div className="flex items-center gap-2">
<ToggleDarkMode hideInMobile />
<DropdownMenu>
<DropdownMenuTrigger className="hidden sm:inline-grid">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button
variant="accent"
size="sm"
className="min-w-[3.4rem] h-[2rem] relative"
>
<span>
<i className="bi-plus text-4xl absolute -top-[0.3rem] left-0 pointer-events-none" />
</span>
<span>
<i className="bi-caret-down-fill text-xs absolute top-[0.6rem] right-[0.4rem] pointer-events-none" />
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("create_new")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</DropdownMenuTrigger>
<DropdownMenu>
<DropdownMenuTrigger className="hidden sm:inline-grid">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button
variant="accent"
size="sm"
className="min-w-[3.4rem] h-[2rem] relative"
>
<span>
<i className="bi-plus text-4xl absolute -top-[0.3rem] left-0 pointer-events-none" />
</span>
<span>
<i className="bi-caret-down-fill text-xs absolute top-[0.6rem] right-[0.4rem] pointer-events-none" />
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("create_new")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => setNewLinkModal(true)}>
<i className="bi-link-45deg" />
{t("new_link")}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setUploadFileModal(true)}>
<i className="bi-file-earmark-arrow-up" />
{t("upload_file")}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setNewCollectionModal(true)}>
<i className="bi-folder" />
{t("new_collection")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => setNewLinkModal(true)}>
<i className="bi-link-45deg" />
{t("new_link")}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setUploadFileModal(true)}>
<i className="bi-file-earmark-arrow-up" />
{t("upload_file")}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setNewCollectionModal(true)}>
<i className="bi-folder" />
{t("new_collection")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ProfileDropdown />
</div>
<MobileNavigation />
{sidebar && (
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-40">
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
<div className="slide-right h-full shadow-lg">
<Sidebar />
</div>
</ClickAwayHandler>
<ProfileDropdown />
</div>
)}
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
{newCollectionModal && (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
)}
{uploadFileModal && (
<UploadFileModal onClose={() => setUploadFileModal(false)} />
)}
</div>
<MobileNavigation />
{sidebar && (
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-40">
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
<div className="slide-right h-full shadow-lg">
<Sidebar />
</div>
</ClickAwayHandler>
</div>
)}
{newLinkModal && (
<NewLinkModal onClose={() => setNewLinkModal(false)} />
)}
{newCollectionModal && (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
)}
{uploadFileModal && (
<UploadFileModal onClose={() => setUploadFileModal(false)} />
)}
</div>
</>
);
}

View File

@@ -1,6 +1,6 @@
import { randomBytes } from "crypto";
import { prisma } from "@linkwarden/prisma";
import transporter from "./transporter";
import transporter from "@linkwarden/lib/transporter";
import Handlebars from "handlebars";
import { readFileSync } from "fs";
import path from "path";
@@ -51,7 +51,7 @@ export default async function sendChangeEmailVerificationRequest(
baseUrl: process.env.BASE_URL,
oldEmail,
newEmail,
verifyUrl: `${process.env.BASE_URL}/auth/verify-email?token=${token}`,
url: `${process.env.BASE_URL}/auth/verify-email?token=${token}`,
}),
});
}

View File

@@ -1,7 +1,7 @@
import { readFileSync } from "fs";
import path from "path";
import Handlebars from "handlebars";
import transporter from "./transporter";
import transporter from "@linkwarden/lib/transporter";
type Params = {
parentSubscriptionEmail: string;

View File

@@ -1,6 +1,6 @@
import { randomBytes } from "crypto";
import { prisma } from "@linkwarden/prisma";
import transporter from "./transporter";
import transporter from "@linkwarden/lib/transporter";
import Handlebars from "handlebars";
import { readFileSync } from "fs";
import path from "path";
@@ -37,7 +37,6 @@ export default async function sendPasswordResetRequest(
subject: "Linkwarden: Reset password instructions",
html: emailTemplate({
user,
baseUrl: process.env.BASE_URL,
url: `${process.env.BASE_URL}/auth/reset-password?token=${token}`,
}),
});

View File

@@ -1,7 +1,7 @@
import { readFileSync } from "fs";
import path from "path";
import Handlebars from "handlebars";
import transporter from "./transporter";
import transporter from "@linkwarden/lib/transporter";
type Params = {
identifier: string;

View File

@@ -1,17 +1,23 @@
import fetch from "node-fetch";
import https from "https";
import http from "http";
import { HttpsProxyAgent } from "https-proxy-agent";
import { SocksProxyAgent } from "socks-proxy-agent";
export default async function fetchTitleAndHeaders(url: string) {
export default async function fetchTitleAndHeaders(
url: string,
content?: string
) {
if (!url?.startsWith("http://") && !url?.startsWith("https://"))
return { title: "", headers: null };
try {
const httpsAgent = new https.Agent({
rejectUnauthorized:
process.env.IGNORE_UNAUTHORIZED_CA === "true" ? false : true,
});
const httpsAgent = url?.startsWith("http://")
? new http.Agent({})
: new https.Agent({
rejectUnauthorized:
process.env.IGNORE_UNAUTHORIZED_CA === "true" ? false : true,
});
// fetchOpts allows a proxy to be defined
let fetchOpts = {
@@ -45,13 +51,20 @@ export default async function fetchTitleAndHeaders(url: string) {
const response = await Promise.race([responsePromise, timeoutPromise]);
if ((response as any)?.status) {
const text = await (response as any).text();
let text: string;
if (content) {
text = content;
} else {
text = await (response as any).text();
}
const headers = (response as Response)?.headers || null;
// regular expression to find the <title> tag
let match = text.match(/<title.*>([^<]*)<\/title>/);
const title = match?.[1] || "";
const headers = (response as Response)?.headers || null;
return { title, headers };
} else {

View File

@@ -1,6 +1,6 @@
{
"name": "@linkwarden/web",
"version": "v2.13.0",
"version": "v2.13.1",
"main": "index.js",
"repository": "https://github.com/linkwarden/linkwarden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>",
@@ -42,7 +42,6 @@
"@types/crypto-js": "^4.1.1",
"@types/formidable": "^3.4.5",
"@types/node": "^20.10.4",
"@types/nodemailer": "^6.4.8",
"@types/papaparse": "^5.3.16",
"@types/react": "18.3.20",
"@types/react-dom": "18.3.7",
@@ -72,7 +71,6 @@
"next-auth": "^4.22.1",
"next-i18next": "^15.3.0",
"node-fetch": "^2.7.0",
"nodemailer": "^6.9.3",
"papaparse": "^5.5.3",
"playwright": "^1.55.0",
"react": "18.3.1",

View File

@@ -146,7 +146,19 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
if (!collection) {
throw new Error("Collection not found.");
}
const { title = "" } = url ? await fetchTitleAndHeaders(url) : {};
// Generate a preview if it's an image
const { mimetype } = files.file[0];
const isPDF = mimetype?.includes("pdf");
const isImage = mimetype?.includes("image");
const isHTML = mimetype === "text/html";
const { title = "" } = url
? await fetchTitleAndHeaders(
url,
isHTML && !isPreview ? fileBuffer.toString("utf-8") : undefined
)
: {};
const link = await prisma.link.create({
data: {
@@ -162,15 +174,14 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
},
},
url,
// temporarily prevent archiveHandler and other processes from overwriting the file while we're uploading it
lastPreserved: new Date(0).toISOString(),
aiTagged: true,
indexVersion: 1,
},
});
// Generate a preview if it's an image
const { mimetype } = files.file[0];
const isPDF = mimetype?.includes("pdf");
const isImage = mimetype?.includes("image");
const isHTML = mimetype === "text/html";
if (isImage) {
const collectionId = collection.id;
createFolder({ filePath: `archives/preview/${collectionId}` });
@@ -203,6 +214,10 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
: undefined,
clientSide: true,
updatedAt: new Date().toISOString(),
lastPreserved: null,
aiTagged: false,
indexVersion: null,
},
});

View File

@@ -1068,6 +1068,35 @@ if (process.env.NEXT_PUBLIC_STRAVA_ENABLED === "true") {
};
}
// Synology
if (process.env.NEXT_PUBLIC_SYNOLOGY_ENABLED === "true") {
providers.push({
id: "synology",
name: "Synology",
type: "oauth",
clientId: process.env.SYNOLOGY_CLIENT_ID!,
clientSecret: process.env.SYNOLOGY_CLIENT_SECRET!,
wellKnown: process.env.SYNOLOGY_WELLKNOWN_URL!,
authorization: { params: { scope: "openid email profile" } },
idToken: true,
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
username: profile.preferred_username,
};
},
});
const _linkAccount = adapter.linkAccount;
adapter.linkAccount = (account) => {
const { "not-before-policy": _, refresh_expires_in, ...data } = account;
return _linkAccount ? _linkAccount(data) : undefined;
};
}
// Todoist
if (process.env.NEXT_PUBLIC_TODOIST_ENABLED === "true") {
providers.push(

View File

@@ -11,7 +11,8 @@ export const getEnvData = () => {
process.env.OPENAI_API_KEY ||
process.env.AZURE_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
process.env.OPENROUTER_API_KEY
process.env.OPENROUTER_API_KEY ||
process.env.PERPLEXITY_API_KEY
);
return {

View File

@@ -335,6 +335,13 @@ export function getLogins() {
name: process.env.STRAVA_CUSTOM_NAME ?? "Strava",
});
}
// Synology
if (process.env.NEXT_PUBLIC_SYNOLOGY_ENABLED === "true") {
buttonAuths.push({
method: "synology",
name: process.env.SYNOLOGY_CUSTOM_NAME ?? "Synology",
});
}
// Todoist
if (process.env.NEXT_PUBLIC_TODOIST_ENABLED === "true") {
buttonAuths.push({

View File

@@ -163,6 +163,7 @@ export default function Login({
</p>
<TextInput
name="username"
autoFocus={true}
placeholder="johnny"
value={form.username}

View File

@@ -9,7 +9,6 @@ import getServerSideProps from "@/lib/client/getServerSideProps";
import { Trans, useTranslation } from "next-i18next";
import { useUser } from "@linkwarden/router/user";
import { Separator } from "@/components/ui/separator";
import Link from "next/link";
import { cn } from "@/lib/utils";
const TRIAL_PERIOD_DAYS =
@@ -122,32 +121,38 @@ export default function Subscribe() {
</div>
</div>
<div className="flex flex-col gap-2 justify-center items-center">
<div className="flex flex-col gap-2 justify-center items-center min-h-36">
<p className="text-3xl">
${plan === Plan.monthly ? "4" : "3"}
<span className="text-base text-neutral">/mo</span>
</p>
<p className="font-semibold">
{plan === Plan.monthly ? t("billed_monthly") : t("billed_yearly")}
</p>
<fieldset className="w-full flex-col flex justify-evenly px-4 pb-4 pt-1 rounded-md border border-neutral-content">
<legend className="w-fit font-extralight px-2 border border-neutral-content rounded-md text-xl">
{t("total")}
</legend>
<p className="text-sm">
{plan === Plan.monthly
? t("total_monthly_desc", {
count: REQUIRE_CC ? 14 : daysLeft,
monthlyPrice: "4",
})
: t("total_annual_desc", {
count: REQUIRE_CC ? 14 : daysLeft,
annualPrice: "36",
})}
</p>
<p className="text-sm">{t("plus_tax")}</p>
</fieldset>
{daysLeft > 0 ? (
<fieldset className="w-full max-h-fit flex-col flex gap-2 px-4 pb-4 pt-2 rounded-md border border-neutral-content">
<legend className="w-fit font-extralight px-2 border border-neutral-content rounded-md text-xl">
{t("total")}
</legend>
<p className="text-sm">
{plan === Plan.monthly
? t("total_monthly_desc", {
count: REQUIRE_CC ? 14 : daysLeft,
monthlyPrice: "4",
})
: t("total_annual_desc", {
count: REQUIRE_CC ? 14 : daysLeft,
annualPrice: "36",
})}
</p>
<p className="text-sm">{t("plus_tax")}</p>
</fieldset>
) : (
<p className="text-xs">{t("plus_tax")}</p>
)}
</div>
<div

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -523,5 +523,7 @@
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar"
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -208,7 +208,7 @@
"password_successfully_updated": "Tu contraseña ha sido actualizada correctamente.",
"user_already_member": "El usuario ya existe.",
"you_are_already_collection_owner": "Ya eres el dueño de esta colección.",
"link_already_in_collection": "This link is already in this collection.",
"link_already_in_collection": "Este enlace ya se encuentra en esta colección.",
"date_newest_first": "Fecha (Nuevos primero)",
"date_oldest_first": "Fecha (Antiguos primero)",
"name_az": "Nombre (A-Z)",
@@ -491,27 +491,39 @@
"no_notes_highlights": "No se han encontrado notas ni destacados para este enlace.",
"save": "Guardar",
"edit_layout": "Editar Disposición",
"refresh_multiple_preserved_formats_confirmation_desc": "This will delete the current preserved formats and re-preserve {{count}} links.",
"refresh_preserved_formats_confirmation_desc": "This will delete the current preserved formats and re-preserve this link.",
"refresh_multiple_preserved_formats_confirmation_desc": "Esto eliminará los formatos conservados actuales y volverá a preservar los {{count}} enlaces.",
"refresh_preserved_formats_confirmation_desc": "Esto eliminará los formatos conservados actuales y reconservará este enlace.",
"tag_already_added": "Esta etiqueta ya ha sido añadida.",
"all_tags": "All Tags",
"new_tag": "New Tag",
"create_new_tag": "Create New Tag",
"tag_deletion_confirmation_message": "Are you sure you want to delete this Tag?",
"delete_tags_by_number_of_links": "Delete Tags with <0/> Links",
"delete_all_tags": "Delete all Tags",
"bulk_delete_tags": "Bulk Delete Tags",
"count_tags_deleted": "{{count}} Tags Deleted",
"count_tag_deleted": "{{count}} Tag Deleted",
"tag_name_placeholder": "e.g. Technology",
"link_count_high_low": "Link count (high to low)",
"link_count_low_high": "Link count (low to high)",
"tags_selected": "{{count}} Tags selected",
"tag_selected": "1 Tag selected",
"merge_tags": "Merge Tags",
"merge_count_tags": "Merge {{count}} Tags",
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"all_tags": "Todas las etiquetas",
"new_tag": "Nueva Etiqueta",
"create_new_tag": "Crear nueva etiqueta",
"tag_deletion_confirmation_message": "¿Está seguro que desea eliminar esta etiqueta?",
"delete_tags_by_number_of_links": "Eliminar etiquetas con <0/> enlaces",
"delete_all_tags": "Eliminar todas las etiquetas",
"bulk_delete_tags": "Eliminar etiquetas en masa",
"count_tags_deleted": "{{count}} etiquetas eliminadas",
"count_tag_deleted": "{{count}} etiqueta eliminada",
"tag_name_placeholder": "ej. Tecnología",
"link_count_high_low": "Contador de enlaces (alto a bajo)",
"link_count_low_high": "Contador de enlaces (bajo a alto)",
"tags_selected": "{{count}} etiquetas seleccionadas",
"tag_selected": "1 etiqueta seleccionada",
"merge_tags": "Fusionar etiquetas",
"merge_count_tags": "Fusionar {{count}} etiquetas",
"rename_tag_instruction": "Por favor, proporcione un nombre para la etiqueta final.",
"merging": "Fusionando...",
"delete_tags": "Eliminar {{count}} etiquetas",
"tags_deletion_confirmation_message": "¿Está seguro de que desea eliminar {{count}} etiquetas? Esto eliminará las etiquetas de todos los enlaces.",
"subscribe_later": "¿Suscribirse más tarde?",
"create_your_first_tag": "¡Crea tu primera etiqueta!",
"create_your_first_tag_desc": "Las etiquetas le ayudan a clasificar y encontrar sus enlaces fácilmente. Puede crear etiquetas basadas en temas, proyectos o cualquier sistema que funcione para usted.",
"create_your_first_collection": "¡Crea tu primera colección!",
"create_your_first_collection_desc": "Las colecciones son como carpetas para sus enlaces que pueden ser compartidas con otros.",
"this_tag_has_no_links": "Esta etiqueta no tiene enlaces",
"this_tag_has_no_links_desc": "¡Usa esta etiqueta mientras crea o edita enlaces!",
"accept_promotional_emails": "Recibe notificaciones sobre nuevas características y ofertas por correo electrónico.",
"expand_sidebar": "Expandir barra lateral",
"shrink_sidebar": "Contraer barra lateral",
"trial_left_plural": "La prueba termina en {{count}} días. Suscríbete.",
"trial_left_singular": "La prueba termina en 1 día. Suscríbete."
}

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -377,12 +377,12 @@
"demo_desc_2": "Als u de volledige versie wilt proberen, kunt u zich aanmelden voor een gratis proefperiode op:",
"demo_button": "Inloggen als demo gebruiker",
"regular": "Regular",
"thin": "Thin",
"thin": "Dun",
"bold": "Bold",
"fill": "Fill",
"duotone": "Duotone",
"light_icon": "Light",
"search": "Search",
"search": "Zoek",
"set_custom_icon": "Set Custom Icon",
"view": "View",
"show": "Show",
@@ -394,7 +394,7 @@
"untitled": "Naamloos",
"no_tags": "No tags.",
"no_description_provided": "No description provided.",
"change_icon": "Change Icon",
"change_icon": "Pas icoon aan",
"upload_banner": "Upload Banner",
"columns": "Kolommen",
"default": "Default",
@@ -419,7 +419,7 @@
"seat_purchased": "{{count}} seat purchased",
"date_added": "Date Added",
"resend_invite": "Resend Invitation",
"resend_invite_success": "Invitation Resent!",
"resend_invite_success": "Uitnodiging opnieuw verstuurd!",
"remove_user": "Remove User",
"continue_to_dashboard": "Continue to Dashboard",
"confirm_user_removal_desc": "They will need to have a subscription to access Linkwarden again.",
@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -494,24 +494,36 @@
"refresh_multiple_preserved_formats_confirmation_desc": "Isto irá apagar os atuais formatos preservados e re-preservar os links {{count}}.",
"refresh_preserved_formats_confirmation_desc": "Isto irá excluir os atuais formatos preservados e preservar este link novamente.",
"tag_already_added": "Esta tag já está adicionada.",
"all_tags": "All Tags",
"new_tag": "New Tag",
"create_new_tag": "Create New Tag",
"tag_deletion_confirmation_message": "Are you sure you want to delete this Tag?",
"delete_tags_by_number_of_links": "Delete Tags with <0/> Links",
"delete_all_tags": "Delete all Tags",
"bulk_delete_tags": "Bulk Delete Tags",
"count_tags_deleted": "{{count}} Tags Deleted",
"count_tag_deleted": "{{count}} Tag Deleted",
"tag_name_placeholder": "e.g. Technology",
"link_count_high_low": "Link count (high to low)",
"link_count_low_high": "Link count (low to high)",
"tags_selected": "{{count}} Tags selected",
"tag_selected": "1 Tag selected",
"merge_tags": "Merge Tags",
"merge_count_tags": "Merge {{count}} Tags",
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"all_tags": "Todas as Etiquetas",
"new_tag": "Nova Etiqueta",
"create_new_tag": "Criar etiqueta",
"tag_deletion_confirmation_message": "Tem certeza de que deseja excluir esta etiqueta?",
"delete_tags_by_number_of_links": "Excluir etiquetas com links <0/>",
"delete_all_tags": "Excluir todas as etiquetas",
"bulk_delete_tags": "Exclusão em massa de etiquetas",
"count_tags_deleted": "{{count}} Etiquetas deletadas",
"count_tag_deleted": "{{count}} Etiqueta deletada",
"tag_name_placeholder": "ex. Tecnologia",
"link_count_high_low": "Qtd. de links (do maior para o menor)",
"link_count_low_high": "Contagem de links (do menor para o maior)",
"tags_selected": "{{count}} Etiquetas selecionadas",
"tag_selected": "Uma etiqueta selecionada",
"merge_tags": "Mesclar Etiquetas",
"merge_count_tags": "Mesclar {{count}} Etiquetas",
"rename_tag_instruction": "Por favor, forneça um nome para a etiqueta final.",
"merging": "Mesclando...",
"delete_tags": "Excluir {{count}} etiquetas",
"tags_deletion_confirmation_message": "Tem certeza que deseja excluir {{count}} etiquetas? Isto irá remover as etiquetas de todos os links.",
"subscribe_later": "Inscrever-se depois?",
"create_your_first_tag": "Crie sua Primeira Etiqueta!",
"create_your_first_tag_desc": "As etiquetas ajudam você a categorizar e encontrar seus links facilmente. Você pode criar etiquetas baseadas em tópicos, projetos ou qualquer sistema que funcione para você.",
"create_your_first_collection": "Crie Sua Primeira Coleção!",
"create_your_first_collection_desc": "Coleções são como pastas para seus links, que podem ser compartilhadas com outras pessoas.",
"this_tag_has_no_links": "Esta etiqueta não possui links",
"this_tag_has_no_links_desc": "Use esta etiqueta enquanto cria ou edita Links!",
"accept_promotional_emails": "Seja notificado sobre novos recursos e ofertas por e-mail.",
"expand_sidebar": "Expandir barra lateral",
"shrink_sidebar": "Encolher barra lateral",
"trial_left_plural": "O período de teste termina em {{count}} dias. Inscreva-se.",
"trial_left_singular": "O período de teste termina em 1 dia. Inscreva-se."
}

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Users Administration",
"user_administration": "Управління користувачами",
"search_users": "Пошук користувачів",
"no_users_found": "Користувачів не знайдено.",
"no_user_found_in_search": "За вказаним пошуковим запитом не знайдено користувачів.",
@@ -42,8 +42,8 @@
"from_linkwarden": "Від Linkwarden",
"from_html": "З HTML-файлу закладок",
"from_wallabag": "Від Wallabag (файл JSON)",
"from_omnivore": "From Omnivore (ZIP file)",
"from_pocket": "From Pocket (CSV file)",
"from_omnivore": "З Omnivore (ZIP-файл)",
"from_pocket": "З Pocket (файл CSV)",
"pinned": "Закріплено",
"pinned_links_desc": "Ваші закріплені посилання",
"pin_favorite_links_here": "Закріпіть тут свої улюблені посилання!",
@@ -208,7 +208,7 @@
"password_successfully_updated": "Ваш пароль успішно оновлено.",
"user_already_member": "Користувач уже існує.",
"you_are_already_collection_owner": "Ви вже є власником колекції.",
"link_already_in_collection": "This link is already in this collection.",
"link_already_in_collection": "Це посилання вже є у цій колекції.",
"date_newest_first": "Дата (спочатку найновіші)",
"date_oldest_first": "Дата (спочатку найдавніші)",
"name_az": "Ім'я, зростання за алфавітом (А-Я)",
@@ -242,7 +242,7 @@
"new_version_announcement": "Подивіться, що нового в <0>Linkwarden {{version}}</0>",
"creating": "Створення...",
"upload_file": "Завантажте файл",
"content": "Content",
"content": "Вміст",
"file_types": "PDF, PNG, JPG (до {{size}} МБ)",
"description": "Опис",
"auto_generated": "Буде створено автоматично, якщо нічого не надано.",
@@ -321,7 +321,7 @@
"sharable_link": "Посилання для спільного використання",
"copied": "Скопійовано!",
"members": "Члени",
"add_member_placeholder": "Add members by email or username",
"add_member_placeholder": "Додати учасників за ел. поштою або ім'ям користувача",
"owner": "Власник",
"admin": "Адмін",
"contributor": "Дописувач",
@@ -343,11 +343,11 @@
"link_deletion_confirmation_message": "Ви впевнені, що хочете видалити це посилання?",
"warning": "Попередження",
"irreversible_warning": "Це незворотна дія!",
"tip": "Tip",
"tip": "Порада",
"shift_key_tip": "Утримуйте клавішу Shift під час натискання 'Видалити', щоб обійти це підтвердження в майбутньому.",
"deleting_collection": "Видалення...",
"collection_deleted": "Колекцію видалено.",
"collection_deletion_prompt": "Are you sure you want to delete this Collection?",
"collection_deletion_prompt": "Ви справді хочете видалити цю колекцію?",
"type_name_placeholder": "Введіть \"{{name}}\" тут.",
"deletion_warning": "Видалення цієї колекції призведе до остаточного видалення всього її вмісту, і вона стане недоступною для всіх, включаючи учасників з попереднім доступом.",
"leave_prompt": "Натисніть кнопку нижче, щоб залишити поточну колекцію.",
@@ -395,7 +395,7 @@
"no_tags": "Без міток.",
"no_description_provided": "Опис не надано.",
"change_icon": "Змінити піктограму",
"upload_banner": "Upload Banner",
"upload_banner": "Завантажити банер",
"columns": "Стовпці",
"default": "За замовчуванням",
"invalid_url_guide": "Будь ласка, введіть дійсну адресу для посилання. (Він має починатися з http/https)",
@@ -434,84 +434,96 @@
"lemmy": "Lemmy",
"people_recommendation": "Рекомендація (друг, родина тощо)",
"open_all_links": "Відкрити всі посилання",
"ai_settings": "AI Settings",
"generate_tags_for_existing_links": "Generate tags for existing Links",
"ai_tagging_method": "AI Tagging Method:",
"based_on_predefined_tags": "Based on predefined Tags",
"based_on_predefined_tags_desc": "Auto-categorize links to predefined tags based on the content of each link.",
"based_on_existing_tags": "Based on existing Tags",
"based_on_existing_tags_desc": "Auto-categorize links to existing tags based on the content of each link.",
"auto_generate_tags": "Auto-generate Tags",
"auto_generate_tags_desc": "Auto-generate relevant tags based on the content of each link.",
"disabled": "Disabled",
"ai_tagging_disabled_desc": "AI tagging is disabled.",
"tag_selection_placeholder": "Choose or add custom tags…",
"rss_subscriptions": "RSS Subscriptions",
"rss_subscriptions_desc": "RSS Subscriptions are a way to keep up with your favorite websites and blogs. Linkwarden will automatically fetch the latest articles every {{number}} minutes from the feeds you provide.",
"rss_deletion_confirmation": "Are you sure you want to delete this RSS Subscription?",
"new_rss_subscription": "New RSS Subscription",
"rss_subscription_deleted": "RSS Subscription deleted!",
"create_rss_subscription": "Create RSS Subscription",
"rss_feed": "RSS Feed",
"pinned_links": "Pinned Links",
"recent_links": "Recent Links",
"search_results": "Search Results",
"linkwarden_icon": "Linkwarden Icon",
"permanent_session": "This is a permanent session",
"locale": "en-US",
"not_found_404": "404 - Not Found",
"collection_publicly_shared": "This collection is being shared publicly.",
"search_for_links": "Search for Links",
"search_query_invalid_symbol": "The search query should not contain '%'.",
"open_modal_new_tab": "Open this modal in a new tab",
"ai_settings": "Налаштування ШІ",
"generate_tags_for_existing_links": "Створити мітки для наявних посилань",
"ai_tagging_method": "Мітки ШІ:",
"based_on_predefined_tags": "На основі заданих міток",
"based_on_predefined_tags_desc": "Автоматично розподіляти посилання до заданих міток на основі вмісту кожного посилання.",
"based_on_existing_tags": "На основі існуючих міток",
"based_on_existing_tags_desc": "Автоматично розподіляти посилання до наявних міток на основі вмісту кожного посилання.",
"auto_generate_tags": "Автостворення міток",
"auto_generate_tags_desc": "Автоматично створювати відповідні мітки на основі вмісту кожного посилання.",
"disabled": "Вимкнено",
"ai_tagging_disabled_desc": "Мітки ШІ вимкнуто.",
"tag_selection_placeholder": "Виберіть або додайте власні мітки…",
"rss_subscriptions": "RSS-підписки",
"rss_subscriptions_desc": "RSS-підписки дозволяють бути в курсі оновлень ваших улюблених сайтів та блогів. Linkwarden автоматично завантажуватиме останні статті з наданих RSS-стрічок кожні {{number}} хвилин.",
"rss_deletion_confirmation": "Ви дійсно бажаєте видалити цю RSS-підписку?",
"new_rss_subscription": "Нова підписка RSS",
"rss_subscription_deleted": "RSS-підписку видалено!",
"create_rss_subscription": "Створити RSS-підписку",
"rss_feed": "Стрічка RSS",
"pinned_links": "Прикріплені посилання",
"recent_links": "Останні посилання",
"search_results": "Результати пошуку",
"linkwarden_icon": "Піктограма Linkwarden",
"permanent_session": "Це постійна сесія",
"locale": "uk-UA",
"not_found_404": "404 Не знайдено",
"collection_publicly_shared": "Цю колекцію поширено публічно.",
"search_for_links": "Шукати посилання",
"search_query_invalid_symbol": "Пошуковий запит не може містити '%'.",
"open_modal_new_tab": "Відкрити спливне вікно у новій вкладці",
"file": "Файл",
"tag_preservation_rule_label": "Preservation rules by tag (override global settings):",
"ai_tagging": "AI Tagging",
"worker": "Worker",
"regenerate_broken_preservations": "Regenerate all broken or missing preservations for all users.",
"delete_all_preservations": "Delete preservation for all webpages, applies to all users.",
"delete_all_preservations_and_regenerate": "Delete and re-preserve all webpages, applies to all users.",
"delete_all_preservation_warning": "This action will delete all existing preservations on the server.",
"no_broken_preservations": "No broken preservations.",
"links_are_being_represerved": "Links are being re-preserved...",
"cancel": "Cancel",
"preservation_rules": "Preservation rules:",
"links_being_archived": "Links are being archived...",
"display_on_dashboard": "Display on Dashboard",
"dashboard_stats": "Dashboard Stats",
"no_results_found": "No results found.",
"no_link_in_collection": "No Link in this Collection",
"no_link_in_collection_desc": "This Collection has no Links yet.",
"theme": "Theme",
"font_style": "Font Style",
"font_size": "Font Size",
"line_height": "Line Height",
"line_width": "Line Width",
"notes_highlights": "Notes & Highlights",
"no_notes_highlights": "No notes or highlights found for this link.",
"save": "Save",
"edit_layout": "Edit Layout",
"refresh_multiple_preserved_formats_confirmation_desc": "This will delete the current preserved formats and re-preserve {{count}} links.",
"refresh_preserved_formats_confirmation_desc": "This will delete the current preserved formats and re-preserve this link.",
"tag_already_added": "This tag is already added.",
"all_tags": "All Tags",
"new_tag": "New Tag",
"create_new_tag": "Create New Tag",
"tag_deletion_confirmation_message": "Are you sure you want to delete this Tag?",
"delete_tags_by_number_of_links": "Delete Tags with <0/> Links",
"delete_all_tags": "Delete all Tags",
"bulk_delete_tags": "Bulk Delete Tags",
"count_tags_deleted": "{{count}} Tags Deleted",
"count_tag_deleted": "{{count}} Tag Deleted",
"tag_name_placeholder": "e.g. Technology",
"link_count_high_low": "Link count (high to low)",
"link_count_low_high": "Link count (low to high)",
"tags_selected": "{{count}} Tags selected",
"tag_selected": "1 Tag selected",
"merge_tags": "Merge Tags",
"merge_count_tags": "Merge {{count}} Tags",
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tag_preservation_rule_label": "Правила збереження за міткою (пріоритетніше загальних налаштувань):",
"ai_tagging": "Мітки ШІ",
"worker": "Працівник",
"regenerate_broken_preservations": "Повторно створити усі пошкоджені та відсутні збереження для усіх користувачів.",
"delete_all_preservations": "Видалити збереження усіх вебсторінок, застосовується до усіх користувачів.",
"delete_all_preservations_and_regenerate": "Видалити та зберегти усі вебсторінки заново, застосовується до усіх користувачів.",
"delete_all_preservation_warning": "Ця дія видалить усі наявні збереження на сервері.",
"no_broken_preservations": "Немає пошкоджених збережень.",
"links_are_being_represerved": "Посилання перезберігаються...",
"cancel": "Скасувати",
"preservation_rules": "Правила зберігання:",
"links_being_archived": "Посилання архівуються...",
"display_on_dashboard": "Показати на панелі",
"dashboard_stats": "Статистика",
"no_results_found": "Результатів не знайдено.",
"no_link_in_collection": "У цій колекції нема посилань",
"no_link_in_collection_desc": "Ця колекція ще не має посилань.",
"theme": "Тема",
"font_style": "Стиль шрифту",
"font_size": "Розмір шрифту",
"line_height": "Висота рядка",
"line_width": "Ширина рядка",
"notes_highlights": "Нотатки та виділення",
"no_notes_highlights": "Для цього посилання не знайдено нотаток чи виділень.",
"save": "Зберегти",
"edit_layout": "Редагувати макет",
"refresh_multiple_preserved_formats_confirmation_desc": "Це видалить усі збережені формати та збереже {{count}} посилань заново.",
"refresh_preserved_formats_confirmation_desc": "Це видалить усі збережені формати та збереже це посилання заново.",
"tag_already_added": "Цю мітку вже додано.",
"all_tags": "Усі мітки",
"new_tag": "Нова мітка",
"create_new_tag": "Створити нову мітку",
"tag_deletion_confirmation_message": "Ви впевнені, що хочете видалити цю мітку?",
"delete_tags_by_number_of_links": "Видалити мітки з <0/> посиланнями",
"delete_all_tags": "Видалити усі мітки",
"bulk_delete_tags": "Масове видалення міток",
"count_tags_deleted": "Міток видалено: {{count}}",
"count_tag_deleted": "Видалено міток: {{count}}",
"tag_name_placeholder": "напр. Технології",
"link_count_high_low": "Кількість посилань (від вищої)",
"link_count_low_high": "Кількість посилань (від нижчої)",
"tags_selected": "Міток видалено: {{count}}",
"tag_selected": "Вибрано 1 мітку",
"merge_tags": "Об'єднати мітки",
"merge_count_tags": "Об'єднати мітки: {{count}}",
"rename_tag_instruction": "Будь ласка, вкажіть назву для мітки.",
"merging": "Об’єднання...",
"delete_tags": "Видалити мітки: {{count}}",
"tags_deletion_confirmation_message": "Ви впевнені, що хочете видалити {{count}} міток? Це видалить мітки з усіх посилань.",
"subscribe_later": "Підписатися пізніше?",
"create_your_first_tag": "Створіть першу мітку!",
"create_your_first_tag_desc": "Мітки допомагають легко категоризувати та знаходити посилання. Створюйте мітки до відповідних тем, проєктів, чи будь-чого іншого.",
"create_your_first_collection": "Створіть свою першу колекцію!",
"create_your_first_collection_desc": "Колекції — це як теки для ваших посилань, якими можна ділитися.",
"this_tag_has_no_links": "Ця мітка не має посилань",
"this_tag_has_no_links_desc": "Скористайтеся цією міткою створюючи та редагуючи посилання!",
"accept_promotional_emails": "Отримувати сповіщення про нові функції та пропозиції на ел. пошту.",
"expand_sidebar": "Розгорнути бічну панель",
"shrink_sidebar": "Згорнути бічну панель",
"trial_left_plural": "Лишилося {{count}} днів пробного доступу. Підпишіться.",
"trial_left_singular": "Лишився 1 день пробного доступу. Підпишіться."
}

View File

@@ -513,5 +513,17 @@
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
"subscribe_later": "Subscribe Later?",
"create_your_first_tag": "Create Your First Tag!",
"create_your_first_tag_desc": "Tags help you categorize and find your Links easily. You can create Tags based on topics, projects, or any system that works for you.",
"create_your_first_collection": "Create Your First Collection!",
"create_your_first_collection_desc": "Collections are like folders for your Links which can then be shared with others.",
"this_tag_has_no_links": "This Tag Has No Links",
"this_tag_has_no_links_desc": "Use this Tag while creating or editing Links!",
"accept_promotional_emails": "Get notified about new features and offers via email.",
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
}

View File

@@ -379,13 +379,13 @@
"regular": "常规",
"thin": "细体",
"bold": "粗体",
"fill": "Fill",
"duotone": "Duotone",
"fill": "填充",
"duotone": "双色调模式",
"light_icon": "亮色",
"search": "搜索",
"set_custom_icon": "设置自定义图标",
"view": "View",
"show": "Show",
"view": "查看",
"show": "显示",
"image": "图片",
"icon": "图标",
"date": "日期",
@@ -396,7 +396,7 @@
"no_description_provided": "没有添加描述。",
"change_icon": "更改图标",
"upload_banner": "上传横幅",
"columns": "Columns",
"columns": "",
"default": "默认",
"invalid_url_guide": "请输入有效的链接地址。(应该以 http/https 开头)",
"email_invalid": "请输入一个有效的电子邮件地址。",
@@ -412,8 +412,8 @@
"invitation_desc": "{{owner}} 已邀请您加入 Linkwarden。\n为继续操作请完成您的账户设置。",
"invitation_accepted": "邀请已接受!",
"status": "状态",
"pending": "Pending",
"active": "Active",
"pending": "待定",
"active": "已启用",
"manage_seats": "管理席位",
"seats_purchased": "已购买 {{count}} 个席位",
"seat_purchased": "已购买 {{count}} 个席位",
@@ -428,11 +428,11 @@
"thanks_for_feedback": "感谢您的反馈!",
"quick_survey": "快速问卷调查",
"how_did_you_discover_linkwarden": "你是如何得知Linkwarden的",
"rather_not_say": "Rather not say",
"rather_not_say": "不愿透露",
"search_engine": "搜索引擎Google、Bing 等)",
"reddit": "Reddit",
"lemmy": "Lemmy",
"people_recommendation": "Recommendation (Friend, Family, etc.)",
"people_recommendation": "推荐(朋友、家人等)",
"open_all_links": "打开所有链接",
"ai_settings": "AI 设置",
"generate_tags_for_existing_links": "为已有链接生成标签",
@@ -494,24 +494,36 @@
"refresh_multiple_preserved_formats_confirmation_desc": "此操作将删除当前已保存的格式,并重新保存 {{count}} 个链接。",
"refresh_preserved_formats_confirmation_desc": "此操作将删除当前已保存的格式,并重新保存此链接。",
"tag_already_added": "此标签已添加。",
"all_tags": "All Tags",
"new_tag": "New Tag",
"create_new_tag": "Create New Tag",
"tag_deletion_confirmation_message": "Are you sure you want to delete this Tag?",
"delete_tags_by_number_of_links": "Delete Tags with <0/> Links",
"all_tags": "所有标签",
"new_tag": "新标签",
"create_new_tag": "创建新标签",
"tag_deletion_confirmation_message": "确定删除此标签?",
"delete_tags_by_number_of_links": "删除 <0/> 链接的标签",
"delete_all_tags": "删除所有标签",
"bulk_delete_tags": "Bulk Delete Tags",
"count_tags_deleted": "{{count}} Tags Deleted",
"count_tag_deleted": "{{count}} Tag Deleted",
"tag_name_placeholder": "e.g. Technology",
"link_count_high_low": "Link count (high to low)",
"link_count_low_high": "Link count (low to high)",
"tags_selected": "{{count}} Tags selected",
"tag_selected": "1 Tag selected",
"merge_tags": "Merge Tags",
"merge_count_tags": "Merge {{count}} Tags",
"rename_tag_instruction": "Please provide a name for the final tag.",
"merging": "Merging...",
"delete_tags": "Delete {{count}} Tags",
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
"bulk_delete_tags": "批量删除标签",
"count_tags_deleted": "已删除 {{count}} 个标签",
"count_tag_deleted": "已删除 {{count}} 个标签",
"tag_name_placeholder": "例如: 技术",
"link_count_high_low": "链接数量(从高到低)",
"link_count_low_high": "链接数量(从低到高)",
"tags_selected": "已选中 {{count}} 个标签",
"tag_selected": "已选中 1 个标签",
"merge_tags": "合并标签",
"merge_count_tags": "合并 {{count}} 个标签",
"rename_tag_instruction": "请为最后一个标签命名。",
"merging": "合并中……",
"delete_tags": "删除 {{count}} 个链接",
"tags_deletion_confirmation_message": "确定删除 {{count}} 个标签?这些标签将从所有链接被删除。",
"subscribe_later": "稍后订阅?",
"create_your_first_tag": "创建您的第一个标签!",
"create_your_first_tag_desc": "标签有助于您更容易地分类和找到您的链接。您可以根据主题、项目或为您服务的任何系统创建标签。",
"create_your_first_collection": "创建您的第一个收藏夹!",
"create_your_first_collection_desc": "收藏夹是您可以与他人分享的链接文件夹。",
"this_tag_has_no_links": "此标签没有链接",
"this_tag_has_no_links_desc": "创建或编辑链接时使用此标签!",
"accept_promotional_emails": "通过电子邮件获得新功能和优惠通知。",
"expand_sidebar": "展开侧边栏",
"shrink_sidebar": "收缩侧边栏",
"trial_left_plural": "试用期将于 {{count}} 天后结束,请订阅。",
"trial_left_singular": "试用期将于 1 天后结束,请订阅。"
}

View File

@@ -1,53 +1,42 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title>Email</title>
<style media="all" type="text/css">
/* Apple Mail / iOS Mail / some webmail */
@media (prefers-color-scheme: dark) {
body {
background-color: #181a1b !important;
color: #ffffff !important;
}
.divider {
border-top: 1px solid #3e3f41 !important;
}
}
/* For Mobile */
@media only screen and (max-width: 640px) {
.main p,
.main td,
.main span {
font-size: 14px !important;
}
.wrapper {
padding: 8px !important;
}
.content {
padding: 0 !important;
}
.container {
padding: 0 !important;
padding-top: 8px !important;
width: 100% !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
.main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
.btn table {
max-width: 100% !important;
width: 100% !important;
}
.btn a {
font-size: 16px !important;
max-width: 100% !important;
width: 100% !important;
}
}
/* For Outlook */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
@@ -56,24 +45,6 @@
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
</style>
</head>
@@ -81,11 +52,11 @@
style="
font-family: Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 16px;
line-height: 1.3;
font-size: 15px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #f8f8f8;
color: black;
margin: 0;
padding: 0;
"
@@ -95,350 +66,255 @@
border="0"
cellpadding="0"
cellspacing="0"
class="body"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
background-color: #f8f8f8;
width: 100%;
"
width="100%"
bgcolor="#f8f8f8"
>
<tr>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
"
valign="top"
>
&nbsp;
</td>
<td
class="container"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
max-width: 600px;
padding: 0;
padding-top: 24px;
width: 600px;
margin: 0 auto;
"
width="600"
valign="top"
>
<div
class="content"
<td style="vertical-align: top" valign="top">&nbsp;</td>
<td class="container" width="600" valign="top" style="padding: 25px">
<!-- Preheader (inbox preview) -->
<span
class="preheader"
style="
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 600px;
padding: 0;
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>
<!-- START CENTERED WHITE CONTAINER -->
<span
class="preheader"
style="
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>Please verify your email address by clicking the button
below.</span
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="main"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
background: #ffffff;
border: 1px solid #eaebed;
border-radius: 16px;
width: 100%;
"
width="100%"
>
<!-- START MAIN CONTENT AREA -->
Please verify your email address by clicking the button below:
</span>
<a href="https://linkwarden.app" target="_blank">
<img
style="width: 50px"
src="https://raw.githubusercontent.com/linkwarden/linkwarden/2c727ccd478b911015b6a0d604c6fca97849c591/assets/logo_small.png"
alt="Linkwarden"
/>
</a>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<h1 style="font-size: 24px; font-weight: bold; margin: 0">
Dear {{identifier}},
</h1>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
You have been invited to join Linkwarden by
{{parentSubscriptionEmail}}!
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
Linkwarden helps teams and individuals collect, read, annotate, and
fully preserve what matters, all in one place.
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn"
style="box-sizing: border-box; width: 100%; min-width: 100%"
width="100%"
>
<tbody>
<tr>
<td
class="wrapper"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
box-sizing: border-box;
padding: 24px;
width: fit-content;
"
valign="top"
>
<h1
style="
font-family: Helvetica, sans-serif;
font-size: 24px;
font-weight: bold;
margin: 0;
margin-bottom: 16px;
"
>
Dear
<a
href="mailto:{{parentSubscriptionEmail}}"
style="color: red"
>{{identifier}}</a
>,
</h1>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
You have been invited to join Linkwarden by
<a
href="mailto:{{parentSubscriptionEmail}}"
style="color: red"
>
{{parentSubscriptionEmail}}</a
>!
</p>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
Linkwarden simplifies digital content management by allowing
teams and individuals to easily collect, organize, and
preserve webpages and articles.
</p>
<td align="left" style="vertical-align: top" valign="top">
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn btn-primary"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
box-sizing: border-box;
width: 100%;
min-width: 100%;
"
width="100%"
>
<tbody>
<tr>
<td
align="left"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
padding-bottom: 16px;
border-radius: 8px;
text-align: center;
background-color: #105f9c;
"
valign="top"
align="center"
bgcolor="#00335a"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
<a
href="{{url}}"
target="_blank"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: auto;
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
color: #ffffff;
"
>
<tbody>
<tr>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 13px;
vertical-align: top;
border-radius: 8px;
text-align: center;
background-color: #00335a;
"
valign="top"
align="center"
bgcolor="#0867ec"
>
<a
href="{{url}}"
target="_blank"
style="
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-size: 13px;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
text-transform: capitalize;
background-color: #00335a;
color: #ffffff;
"
>
Accept Invitation
</a>
</td>
</tr>
</tbody>
</table>
Verify Email
</a>
</td>
</tr>
</tbody>
</table>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
Please note that your Linkwarden account and billing will be
managed by
<a
href="mailto:{{parentSubscriptionEmail}}"
style="color: red"
>
{{parentSubscriptionEmail}}</a
>.
</p>
<hr
style="
border: none;
border-top: 1px solid #eaebed;
margin-bottom: 24px;
width: 100%;
"
/>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 12px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
color: #868686;
"
>
If youre having trouble clicking the button, click on the
following link:
</p>
<span
style="
font-family: Helvetica, sans-serif;
font-size: 10px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
word-break: break-all;
"
>
{{url}}
</span>
</td>
</tr>
</tbody>
</table>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<!-- START FOOTER -->
<div
class="footer"
style="
clear: both;
padding-top: 24px;
text-align: center;
width: 100%;
"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
width="100%"
>
<tr>
<td
class="content-block"
style="vertical-align: top; text-align: center"
valign="top"
align="center"
>
<img
src="https://raw.githubusercontent.com/linkwarden/linkwarden/main/apps/web/public/linkwarden_light.png"
alt="logo"
style="width: 180px; height: auto"
/>
</td>
</tr>
</table>
</div>
<p style="font-weight: normal; margin: 0">
Please note that your Linkwarden account and billing will be managed
by
<a href="mailto:{{parentSubscriptionEmail}}" style="color: red">
{{parentSubscriptionEmail}}</a
>.
</p>
<!-- END FOOTER -->
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
"
valign="top"
>
&nbsp;
<hr
class="divider"
style="border: none; border-top: 1px solid #eaebed; width: 100%"
/>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p
style="
font-size: 12px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
color: #868686;
"
>
If youre having trouble clicking the button, click on the following
link:
</p>
<a
style="
font-size: 10px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
word-break: break-all;
color: rgb(89, 89, 179);
text-decoration: underline;
"
href="{{url}}"
>
{{url}}
</a>
</td>
<td style="vertical-align: top" valign="top">&nbsp;</td>
</tr>
</table>
</body>

View File

@@ -1,53 +1,42 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title>Email</title>
<style media="all" type="text/css">
/* Apple Mail / iOS Mail / some webmail */
@media (prefers-color-scheme: dark) {
body {
background-color: #181a1b !important;
color: #ffffff !important;
}
.divider {
border-top: 1px solid #3e3f41 !important;
}
}
/* For Mobile */
@media only screen and (max-width: 640px) {
.main p,
.main td,
.main span {
font-size: 14px !important;
}
.wrapper {
padding: 8px !important;
}
.content {
padding: 0 !important;
}
.container {
padding: 0 !important;
padding-top: 8px !important;
width: 100% !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
.main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
.btn table {
max-width: 100% !important;
width: 100% !important;
}
.btn a {
font-size: 16px !important;
max-width: 100% !important;
width: 100% !important;
}
}
/* For Outlook */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
@@ -56,24 +45,6 @@
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
</style>
</head>
@@ -81,11 +52,11 @@
style="
font-family: Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 16px;
line-height: 1.3;
font-size: 15px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #f8f8f8;
color: black;
margin: 0;
padding: 0;
"
@@ -95,293 +66,249 @@
border="0"
cellpadding="0"
cellspacing="0"
class="body"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
background-color: #f8f8f8;
width: 100%;
"
width="100%"
bgcolor="#f8f8f8"
>
<tr>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
"
valign="top"
>
&nbsp;
</td>
<td
class="container"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
max-width: 600px;
padding: 0;
padding-top: 24px;
width: 600px;
margin: 0 auto;
"
width="600"
valign="top"
>
<div
class="content"
<td style="vertical-align: top" valign="top">&nbsp;</td>
<td class="container" width="600" valign="top" style="padding: 25px">
<!-- Preheader (inbox preview) -->
<span
class="preheader"
style="
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 600px;
padding: 0;
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>
<!-- START CENTERED WHITE CONTAINER -->
<span
class="preheader"
style="
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>Reset your password?</span
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="main"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
background: #ffffff;
border: 1px solid #eaebed;
border-radius: 16px;
width: 100%;
"
width="100%"
>
<!-- START MAIN CONTENT AREA -->
You requested to change your password. Please click the button below
to proceed.
</span>
<a href="https://linkwarden.app" target="_blank">
<img
style="width: 50px"
src="https://raw.githubusercontent.com/linkwarden/linkwarden/2c727ccd478b911015b6a0d604c6fca97849c591/assets/logo_small.png"
alt="Linkwarden"
/>
</a>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<h1 style="font-size: 24px; font-weight: bold; margin: 0">
Reset your password?
</h1>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">Hi {{user}}!</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
Someone has requested a link to change your password.
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn"
style="box-sizing: border-box; width: 100%; min-width: 100%"
width="100%"
>
<tbody>
<tr>
<td
class="wrapper"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
box-sizing: border-box;
padding: 24px;
width: fit-content;
"
valign="top"
>
<h1
style="
font-family: Helvetica, sans-serif;
font-size: 24px;
font-weight: bold;
margin: 0;
margin-bottom: 16px;
"
>
Reset your password?
</h1>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
Hi {{user}}!
</p>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
Someone has requested a link to change your password.
</p>
<td align="left" style="vertical-align: top" valign="top">
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn btn-primary"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
box-sizing: border-box;
width: 100%;
min-width: 100%;
"
width="100%"
>
<tbody>
<tr>
<td
align="left"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
padding-bottom: 16px;
border-radius: 8px;
text-align: center;
background-color: #105f9c;
"
valign="top"
align="center"
bgcolor="#00335a"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
<a
href="{{url}}"
target="_blank"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: auto;
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
color: #ffffff;
"
>
<tbody>
<tr>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 13px;
vertical-align: top;
border-radius: 8px;
text-align: center;
background-color: #00335a;
"
valign="top"
align="center"
bgcolor="#0867ec"
>
<a
href="{{url}}"
target="_blank"
style="
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-size: 13px;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
text-transform: capitalize;
background-color: #00335a;
color: #ffffff;
"
>Change Password</a
>
</td>
</tr>
</tbody>
</table>
Change Password
</a>
</td>
</tr>
</tbody>
</table>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
If you didn't request this, you can safely ignore this email
and your password will not be changed.
</p>
</td>
</tr>
</tbody>
</table>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<!-- START FOOTER -->
<div
class="footer"
style="
clear: both;
padding-top: 24px;
text-align: center;
width: 100%;
"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
width="100%"
>
<tr>
<td
class="content-block"
style="vertical-align: top; text-align: center"
valign="top"
align="center"
>
<img
src="https://raw.githubusercontent.com/linkwarden/linkwarden/main/apps/web/public/linkwarden_light.png"
alt="logo"
style="width: 180px; height: auto"
/>
</td>
</tr>
</table>
</div>
<p style="font-weight: normal; margin: 0">
If you didn't request this, you can safely ignore this email and
your password will not be changed.
</p>
<!-- END FOOTER -->
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
"
valign="top"
>
&nbsp;
<hr
class="divider"
style="border: none; border-top: 1px solid #eaebed; width: 100%"
/>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p
style="
font-size: 12px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
color: #868686;
"
>
If youre having trouble clicking the button, click on the following
link:
</p>
<a
style="
font-size: 10px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
word-break: break-all;
color: rgb(89, 89, 179);
text-decoration: underline;
"
href="{{url}}"
>
{{url}}
</a>
</td>
<td style="vertical-align: top" valign="top">&nbsp;</td>
</tr>
</table>
</body>

View File

@@ -1,53 +1,42 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title>Email</title>
<style media="all" type="text/css">
/* Apple Mail / iOS Mail / some webmail */
@media (prefers-color-scheme: dark) {
body {
background-color: #181a1b !important;
color: #ffffff !important;
}
.divider {
border-top: 1px solid #3e3f41 !important;
}
}
/* For Mobile */
@media only screen and (max-width: 640px) {
.main p,
.main td,
.main span {
font-size: 14px !important;
}
.wrapper {
padding: 8px !important;
}
.content {
padding: 0 !important;
}
.container {
padding: 0 !important;
padding-top: 8px !important;
width: 100% !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
.main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
.btn table {
max-width: 100% !important;
width: 100% !important;
}
.btn a {
font-size: 16px !important;
max-width: 100% !important;
width: 100% !important;
}
}
/* For Outlook */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
@@ -56,24 +45,6 @@
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
</style>
</head>
@@ -81,11 +52,11 @@
style="
font-family: Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 16px;
line-height: 1.3;
font-size: 15px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #f8f8f8;
color: black;
margin: 0;
padding: 0;
"
@@ -95,318 +66,232 @@
border="0"
cellpadding="0"
cellspacing="0"
class="body"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
background-color: #f8f8f8;
width: 100%;
"
width="100%"
bgcolor="#f8f8f8"
>
<tr>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
"
valign="top"
>
&nbsp;
</td>
<td
class="container"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
max-width: 600px;
padding: 0;
padding-top: 24px;
width: 600px;
margin: 0 auto;
"
width="600"
valign="top"
>
<div
class="content"
<td style="vertical-align: top" valign="top">&nbsp;</td>
<td class="container" width="600" valign="top" style="padding: 25px">
<!-- Preheader (inbox preview) -->
<span
class="preheader"
style="
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 600px;
padding: 0;
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>
<!-- START CENTERED WHITE CONTAINER -->
<span
class="preheader"
style="
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>Please verify your email address by clicking the button
below.</span
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="main"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
background: #ffffff;
border: 1px solid #eaebed;
border-radius: 16px;
width: 100%;
"
width="100%"
>
<!-- START MAIN CONTENT AREA -->
Please verify your email address by clicking the button below:
</span>
<a href="https://linkwarden.app" target="_blank">
<img
style="width: 50px"
src="https://raw.githubusercontent.com/linkwarden/linkwarden/2c727ccd478b911015b6a0d604c6fca97849c591/assets/logo_small.png"
alt="Linkwarden"
/>
</a>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<h1 style="font-size: 24px; font-weight: bold; margin: 0">
Verify your email address
</h1>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
Thank you for signing up with Linkwarden! 🥳
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
Just one small step left. Simply verify your email address, and
youre all set!
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn"
style="box-sizing: border-box; width: 100%; min-width: 100%"
width="100%"
>
<tbody>
<tr>
<td
class="wrapper"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
box-sizing: border-box;
padding: 24px;
width: fit-content;
"
valign="top"
>
<h1
style="
font-family: Helvetica, sans-serif;
font-size: 24px;
font-weight: bold;
margin: 0;
margin-bottom: 16px;
"
>
Verify your email address
</h1>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
Thank you for signing up with Linkwarden! 🥳
</p>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
Just one small step left. Simply verify your email address,
and youre all set!
</p>
<td align="left" style="vertical-align: top" valign="top">
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn btn-primary"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
box-sizing: border-box;
width: 100%;
min-width: 100%;
"
width="100%"
>
<tbody>
<tr>
<td
align="left"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
padding-bottom: 16px;
border-radius: 8px;
text-align: center;
background-color: #105f9c;
"
valign="top"
align="center"
bgcolor="#00335a"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
<a
href="{{url}}"
target="_blank"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: auto;
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
color: #ffffff;
"
>
<tbody>
<tr>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 13px;
vertical-align: top;
border-radius: 8px;
text-align: center;
background-color: #00335a;
"
valign="top"
align="center"
bgcolor="#0867ec"
>
<a
href="{{url}}"
target="_blank"
style="
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-size: 13px;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
text-transform: capitalize;
background-color: #00335a;
color: #ffffff;
"
>Verify Email</a
>
</td>
</tr>
</tbody>
</table>
Verify Email
</a>
</td>
</tr>
</tbody>
</table>
<hr
style="
border: none;
border-top: 1px solid #eaebed;
margin-bottom: 24px;
width: 100%;
"
/>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 12px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
color: #868686;
"
>
If youre having trouble clicking the button, click on the
following link:
</p>
<span
style="
font-family: Helvetica, sans-serif;
font-size: 10px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
word-break: break-all;
"
>
{{url}}
</span>
</td>
</tr>
</tbody>
</table>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<!-- START FOOTER -->
<div
class="footer"
style="
clear: both;
padding-top: 24px;
text-align: center;
width: 100%;
"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
width="100%"
>
<tr>
<td
class="content-block"
style="vertical-align: top; text-align: center"
valign="top"
align="center"
>
<img
src="https://raw.githubusercontent.com/linkwarden/linkwarden/main/apps/web/public/linkwarden_light.png"
alt="logo"
style="width: 180px; height: auto"
/>
</td>
</tr>
</table>
</div>
<hr
class="divider"
style="border: none; border-top: 1px solid #eaebed; width: 100%"
/>
<!-- END FOOTER -->
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
"
valign="top"
>
&nbsp;
<p
style="
font-size: 12px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
color: #868686;
"
>
If youre having trouble clicking the button, click on the following
link:
</p>
<a
style="
font-size: 10px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
word-break: break-all;
color: rgb(89, 89, 179);
text-decoration: underline;
"
href="{{url}}"
>
{{url}}
</a>
</td>
<td style="vertical-align: top" valign="top">&nbsp;</td>
</tr>
</table>
</body>

View File

@@ -1,53 +1,42 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title>Email</title>
<style media="all" type="text/css">
/* Apple Mail / iOS Mail / some webmail */
@media (prefers-color-scheme: dark) {
body {
background-color: #181a1b !important;
color: #ffffff !important;
}
.divider {
border-top: 1px solid #3e3f41 !important;
}
}
/* For Mobile */
@media only screen and (max-width: 640px) {
.main p,
.main td,
.main span {
font-size: 14px !important;
}
.wrapper {
padding: 8px !important;
}
.content {
padding: 0 !important;
}
.container {
padding: 0 !important;
padding-top: 8px !important;
width: 100% !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
.main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
.btn table {
max-width: 100% !important;
width: 100% !important;
}
.btn a {
font-size: 16px !important;
max-width: 100% !important;
width: 100% !important;
}
}
/* For Outlook */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
@@ -56,24 +45,6 @@
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
</style>
</head>
@@ -81,11 +52,11 @@
style="
font-family: Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 16px;
line-height: 1.3;
font-size: 15px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #f8f8f8;
color: black;
margin: 0;
padding: 0;
"
@@ -95,329 +66,270 @@
border="0"
cellpadding="0"
cellspacing="0"
class="body"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
background-color: #f8f8f8;
width: 100%;
"
width="100%"
bgcolor="#f8f8f8"
>
<tr>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
"
valign="top"
>
&nbsp;
</td>
<td
class="container"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
max-width: 600px;
padding: 0;
padding-top: 24px;
width: 600px;
margin: 0 auto;
"
width="600"
valign="top"
>
<div
class="content"
<td style="vertical-align: top" valign="top">&nbsp;</td>
<td class="container" width="600" valign="top" style="padding: 25px">
<!-- Preheader (inbox preview) -->
<span
class="preheader"
style="
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 600px;
padding: 0;
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>
<!-- START CENTERED WHITE CONTAINER -->
<span
class="preheader"
style="
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>You recently requested to change the email address associated
with your Linkwarden account. To verify your new email address,
please click the button below.</span
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="main"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
background: #ffffff;
border: 1px solid #eaebed;
border-radius: 16px;
width: 100%;
"
width="100%"
>
<!-- START MAIN CONTENT AREA -->
You requested to change your email address. Please verify your new
email by clicking the button below.
</span>
<a href="https://linkwarden.app" target="_blank">
<img
style="width: 50px"
src="https://raw.githubusercontent.com/linkwarden/linkwarden/2c727ccd478b911015b6a0d604c6fca97849c591/assets/logo_small.png"
alt="Linkwarden"
/>
</a>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<h1 style="font-size: 24px; font-weight: bold; margin: 0">
Verify your new Linkwarden email address
</h1>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">Hi {{user}}!</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
You recently requested to change the email address associated with
your <a href="{{baseUrl}}">Linkwarden</a> account. To verify your
new email address, please click the button below.
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: bold; margin: 0; margin-bottom: 5px">
Old Email:
</p>
<p style="font-weight: normal; margin: 0">{{oldEmail}}</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: bold; margin: 0; margin-bottom: 5px">
New Email:
</p>
<p style="font-weight: normal; margin: 0">{{newEmail}}</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn"
style="box-sizing: border-box; width: 100%; min-width: 100%"
width="100%"
>
<tbody>
<tr>
<td
class="wrapper"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
box-sizing: border-box;
padding: 24px;
"
valign="top"
>
<h1
style="
font-family: Helvetica, sans-serif;
font-size: 24px;
font-weight: bold;
margin: 0;
margin-bottom: 16px;
"
>
Verify your new Linkwarden email address
</h1>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
Hi {{user}}!
</p>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
You recently requested to change the email address
associated with your
<a href="{{baseUrl}}">Linkwarden</a> account. To verify your
new email address, please click the button below.
</p>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
"
>
<b>Old Email:</b>
</p>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
{{oldEmail}}
</p>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
"
>
<b>New Email:</b>
</p>
<p
style="
font-family: Helvetica, sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
"
>
{{newEmail}}
</p>
<td align="left" style="vertical-align: top" valign="top">
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn btn-primary"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
box-sizing: border-box;
width: 100%;
min-width: 100%;
"
width="100%"
>
<tbody>
<tr>
<td
align="left"
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
padding-bottom: 16px;
border-radius: 8px;
text-align: center;
background-color: #105f9c;
"
valign="top"
align="center"
bgcolor="#00335a"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
<a
href="{{url}}"
target="_blank"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: auto;
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
color: #ffffff;
"
>
<tbody>
<tr>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 13px;
vertical-align: top;
border-radius: 8px;
text-align: center;
background-color: #00335a;
"
valign="top"
align="center"
bgcolor="#0867ec"
>
<a
href="{{verifyUrl}}"
target="_blank"
style="
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-size: 13px;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
text-transform: capitalize;
background-color: #00335a;
color: #ffffff;
"
>Verify Email</a
>
</td>
</tr>
</tbody>
</table>
Verify New Email
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<!-- START FOOTER -->
<div
class="footer"
style="
clear: both;
padding-top: 24px;
text-align: center;
width: 100%;
"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
width="100%"
>
<tr>
<td
class="content-block"
style="vertical-align: top; text-align: center"
valign="top"
align="center"
>
<img
src="https://raw.githubusercontent.com/linkwarden/linkwarden/main/apps/web/public/linkwarden_light.png"
alt="logo"
style="width: 180px; height: auto"
/>
</td>
</tr>
</table>
</div>
<hr
class="divider"
style="border: none; border-top: 1px solid #eaebed; width: 100%"
/>
<!-- END FOOTER -->
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td
style="
font-family: Helvetica, sans-serif;
font-size: 16px;
vertical-align: top;
"
valign="top"
>
&nbsp;
<p
style="
font-size: 12px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
color: #868686;
"
>
If youre having trouble clicking the button, click on the following
link:
</p>
<a
style="
font-size: 10px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
word-break: break-all;
color: rgb(89, 89, 179);
text-decoration: underline;
"
href="{{url}}"
>
{{url}}
</a>
</td>
<td style="vertical-align: top" valign="top">&nbsp;</td>
</tr>
</table>
</body>

View File

@@ -3,7 +3,7 @@ import { prisma } from "@linkwarden/prisma";
import sendToWayback from "./preservationScheme/sendToWayback";
import { AiTaggingMethod } from "@linkwarden/prisma/client";
import fetchHeaders from "./fetchHeaders";
import { createFolder, removeFiles } from "@linkwarden/filesystem";
import { createFolder, readFile, removeFiles } from "@linkwarden/filesystem";
import handleMonolith from "./preservationScheme/handleMonolith";
import handleReadability from "./preservationScheme/handleReadability";
import handleArchivePreview from "./preservationScheme/handleArchivePreview";
@@ -46,7 +46,8 @@ export default async function archiveHandler(
process.env.OPENAI_API_KEY ||
process.env.AZURE_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
process.env.OPENROUTER_API_KEY)
process.env.OPENROUTER_API_KEY ||
process.env.PERPLEXITY_API_KEY)
? true
: undefined,
},
@@ -119,6 +120,26 @@ export default async function archiveHandler(
} else if (link.url) {
await page.goto(link.url, { waitUntil: "domcontentloaded" });
// Handle Monolith being sent in beforehand while making sure other values line up
if (link.monolith?.endsWith(".html")) {
// Use Monolith content instead of page
const file = await readFile(link.monolith);
if (file.contentType == "text/html") {
const fileContent = file.file;
if (typeof fileContent === "string") {
await page.setContent(fileContent, {
waitUntil: "domcontentloaded",
});
} else {
await page.setContent(fileContent.toString("utf-8"), {
waitUntil: "domcontentloaded",
});
}
}
}
const metaDescription = await page.evaluate(() => {
const description = document.querySelector(
'meta[name="description"]'
@@ -152,7 +173,8 @@ export default async function archiveHandler(
process.env.OPENAI_API_KEY ||
process.env.AZURE_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
process.env.OPENROUTER_API_KEY)
process.env.OPENROUTER_API_KEY ||
process.env.PERPLEXITY_API_KEY)
) {
await autoTagLink(user, link.id, metaDescription);
}

View File

@@ -10,6 +10,7 @@ import {
createOpenAICompatible,
OpenAICompatibleProviderSettings,
} from "@ai-sdk/openai-compatible";
import { perplexity } from "@ai-sdk/perplexity";
import { azure } from "@ai-sdk/azure";
import { z } from "zod";
import { anthropic } from "@ai-sdk/anthropic";
@@ -61,6 +62,9 @@ const getAIModel = (): LanguageModelV1 => {
return openrouter(process.env.OPENROUTER_MODEL) as LanguageModelV1;
}
if (process.env.PERPLEXITY_API_KEY) {
return perplexity(process.env.PERPLEXITY_MODEL || "sonar-pro");
}
throw new Error("No AI provider configured");
};

View File

@@ -1,5 +1,6 @@
import fetch from "node-fetch";
import https from "https";
import http from "http";
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from "socks-proxy-agent";
@@ -7,10 +8,12 @@ export default async function fetchHeaders(url: string) {
if (process.env.IGNORE_URL_SIZE_LIMIT === "true") return null;
try {
const httpsAgent = new https.Agent({
rejectUnauthorized:
process.env.IGNORE_UNAUTHORIZED_CA === "true" ? false : true,
});
const httpsAgent = url.startsWith("http://")
? new http.Agent({})
: new https.Agent({
rejectUnauthorized:
process.env.IGNORE_UNAUTHORIZED_CA === "true" ? false : true,
});
let fetchOpts = {
method: "HEAD",

View File

@@ -12,6 +12,7 @@
"@ai-sdk/anthropic": "1.1.5",
"@ai-sdk/azure": "1.1.5",
"@ai-sdk/openai-compatible": "^0.2.13",
"@ai-sdk/perplexity": "1.1.9",
"@linkwarden/filesystem": "*",
"@linkwarden/lib": "*",
"@linkwarden/prisma": "*",
@@ -30,6 +31,7 @@
"rss-parser": "^3.13.0",
"socks-proxy-agent": "^8.0.2",
"tsx": "^4.19.3",
"handlebars": "^4.7.8",
"zod": "^3.23.8"
},
"devDependencies": {

View File

@@ -0,0 +1,334 @@
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title>Email</title>
<style media="all" type="text/css">
/* Apple Mail / iOS Mail / some webmail */
@media (prefers-color-scheme: dark) {
body {
background-color: #181a1b !important;
color: #ffffff !important;
}
.divider {
border-top: 1px solid #3e3f41 !important;
}
}
/* For Mobile */
@media only screen and (max-width: 640px) {
.container {
width: 100% !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
.btn a {
font-size: 16px !important;
max-width: 100% !important;
width: 100% !important;
}
}
/* For Outlook */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
}
</style>
</head>
<body
style="
font-family: Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 15px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #f8f8f8;
color: black;
margin: 0;
padding: 0;
"
>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
width="100%"
>
<tr>
<td style="vertical-align: top" valign="top">&nbsp;</td>
<td class="container" width="600" valign="top" style="padding: 25px">
<!-- Preheader (inbox preview) -->
<span
class="preheader"
style="
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>
Your free trial has ended. Upgrade your account to continue using
Linkwarden.
</span>
<a href="https://linkwarden.app" target="_blank">
<img
style="width: 50px"
src="https://raw.githubusercontent.com/linkwarden/linkwarden/2c727ccd478b911015b6a0d604c6fca97849c591/assets/logo_small.png"
alt="Linkwarden"
/>
</a>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<h1 style="font-size: 24px; font-weight: bold; margin: 0">
Your Linkwarden free trial has ended
</h1>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">Hi {{name}},</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
Thanks for trying Linkwarden! Your free trial has now ended, and
some features are no longer available.
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
To continue saving, organizing, and accessing your links and
preserved data, please upgrade your account.
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="30" style="line-height: 30px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
class="btn"
style="box-sizing: border-box; width: 100%; min-width: 100%"
width="100%"
>
<tbody>
<tr>
<td align="left" style="vertical-align: top" valign="top">
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
>
<tbody>
<tr>
<td
style="
vertical-align: top;
border-radius: 8px;
text-align: center;
background-color: #105f9c;
"
valign="top"
align="center"
bgcolor="#00335a"
>
<a
href="{{url}}"
target="_blank"
style="
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin: 0;
padding: 10px 18px;
text-decoration: none;
color: #ffffff;
"
>
Upgrade Your Account
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p style="font-weight: normal; margin: 0">
Have questions? Just reply, were happy to help!
</p>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<hr
class="divider"
style="border: none; border-top: 1px solid #eaebed; width: 100%"
/>
<!-- SPACER -->
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
>
<tr>
<td height="20" style="line-height: 20px; font-size: 0">
&nbsp;
</td>
</tr>
</table>
<p
style="
font-size: 12px;
font-weight: normal;
margin: 0;
margin-bottom: 5px;
color: #868686;
"
>
If youre having trouble clicking the button, click on the following
link:
</p>
<a
style="
font-size: 10px;
font-weight: normal;
margin: 0;
margin-bottom: 16px;
word-break: break-all;
color: rgb(89, 89, 179);
text-decoration: underline;
"
href="{{url}}"
>
{{url}}
</a>
</td>
<td style="vertical-align: top" valign="top">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,6 +1,7 @@
import { startIndexing } from "./workers/linkIndexing";
import { linkProcessing } from "./workers/linkProcessing";
import { startRSSPolling } from "./workers/rssPolling";
import { trialEndEmailWorker } from "./workers/trialEndEmailWorker";
const workerIntervalInSeconds =
Number(process.env.ARCHIVE_SCRIPT_INTERVAL) || 10;
@@ -10,6 +11,7 @@ async function init() {
startRSSPolling();
linkProcessing(workerIntervalInSeconds);
startIndexing(workerIntervalInSeconds);
trialEndEmailWorker();
}
init();

View File

@@ -1,4 +1,3 @@
import { prisma } from "@linkwarden/prisma";
import archiveHandler from "../lib/archiveHandler";
import { LinkWithCollectionOwnerAndTags } from "@linkwarden/types";
import { delay } from "@linkwarden/lib";

View File

@@ -0,0 +1,125 @@
import { prisma } from "@linkwarden/prisma";
import transporter from "@linkwarden/lib/transporter";
import Handlebars from "handlebars";
import { readFileSync } from "fs";
import path from "path";
const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
const batchSize = 10;
const pauseMs = 30000;
/**
* Runs the "trial ended" notifier in batches.
* - Only runs if NEXT_PUBLIC_TRIAL_PERIOD_DAYS > 1
* - Picks users whose createdAt is older than that trial window AND trialEndEmailSent=false
* - Skips users with an active own or parent subscription
* - Sends email via the provided transporter
* - Marks trialEndEmailSent=true for processed users (including those with active subs)
* - Waits for a minute between batches
*/
export async function trialEndEmailWorker() {
const trialDays = Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS);
if (
!Number.isFinite(trialDays) ||
trialDays <= 1 ||
!process.env.STRIPE_SECRET_KEY ||
process.env.NEXT_PUBLIC_REQUIRE_CC === "true"
) {
return;
}
console.log("\x1b[34m%s\x1b[0m", `Starting trial-ended email worker...`);
const cutoff = new Date(Date.now() - trialDays * 24 * 60 * 60 * 1000);
const from = {
name: "Linkwarden",
address: String(process.env.EMAIL_FROM),
};
while (true) {
// 1) Pick a batch of candidates
const candidates = await prisma.user.findMany({
where: {
trialEndEmailSent: false,
emailVerified: {
not: null,
},
createdAt: { lte: cutoff, gte: new Date("2025-09-25") }, // safety upper bound to avoid processing old users
},
orderBy: { createdAt: "asc" },
take: batchSize,
include: {
subscriptions: { select: { active: true } }, // own subscription
parentSubscription: { select: { active: true } }, // family/parent plan
},
});
if (candidates.length === 0) {
await sleep(pauseMs);
continue;
}
const processedIds: number[] = [];
let emailsSent = 0;
// 2) Process the batch (send emails if no active sub)
for (const user of candidates) {
const hasActive =
Boolean(user.subscriptions?.active) ||
Boolean(user.parentSubscription?.active);
// Well mark users as processed at the end of this loop iteration.
// If sending fails, we skip marking so we retry next pass.
if (!hasActive && user.email) {
emailsSent++;
try {
const emailsDir = path.resolve(process.cwd(), "templates");
const templateFile = readFileSync(
path.join(emailsDir, "trialEnded.html"),
"utf8"
);
const emailTemplate = Handlebars.compile(templateFile);
await transporter.sendMail({
from,
to: user.email,
subject: "Your Linkwarden trial has ended",
html: emailTemplate({
name: user.name?.trim() ? user.name.trim() : "there",
url: process.env.BASE_URL,
}),
});
} catch (err) {
console.error(
`[trial-worker] Failed to send trial-ended email to user ${user.id}`,
err
);
// Do not mark as processed so it can be retried on the next batch run
await sleep(pauseMs);
continue;
}
}
// Whether we emailed or skipped (active sub, or missing email), mark processed
processedIds.push(user.id);
}
// 3) Mark processed users so we don't pick them again
if (processedIds.length) {
await prisma.user.updateMany({
where: { id: { in: processedIds } },
data: { trialEndEmailSent: true },
});
console.log(
"\x1b[34m%s\x1b[0m",
`Marked off ${processedIds.length} users' trialEndEmailSent to true. Emails sent: ${emailsSent}`
);
}
// 4) Pause before the next batch
await sleep(pauseMs);
}
}

BIN
assets/logo_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -11,6 +11,8 @@
"meilisearch": "^0.48.2",
"rss-parser": "^3.13.0",
"clsx": "^2.1.1",
"tailwind-merge": "^3.3.0"
"tailwind-merge": "^3.3.0",
"nodemailer": "^6.9.3",
"@types/nodemailer": "^6.4.8"
}
}

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "trialEndEmailSent" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -69,6 +69,7 @@ model User {
dashboardSections DashboardSection[]
lastPickedAt DateTime?
acceptPromotionalEmails Boolean @default(false)
trialEndEmailSent Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}

View File

@@ -45,6 +45,14 @@
"@ai-sdk/provider" "1.0.6"
"@ai-sdk/provider-utils" "2.1.5"
"@ai-sdk/perplexity@1.1.9":
version "1.1.9"
resolved "https://registry.yarnpkg.com/@ai-sdk/perplexity/-/perplexity-1.1.9.tgz#99542573a4ac240e513c3b7de99359a77a7797a7"
integrity sha512-Ytolh/v2XupXbTvjE18EFBrHLoNMH0Ueji3lfSPhCoRUfkwrgZ2D9jlNxvCNCCRiGJG5kfinSHvzrH5vGDklYA==
dependencies:
"@ai-sdk/provider" "1.1.3"
"@ai-sdk/provider-utils" "2.2.8"
"@ai-sdk/provider-utils@2.1.10":
version "2.1.10"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.1.10.tgz#dfd671ccda12e321b58f347b6cbbdd982d139359"
@@ -74,6 +82,15 @@
nanoid "^3.3.8"
secure-json-parse "^2.7.0"
"@ai-sdk/provider-utils@2.2.8":
version "2.2.8"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz#ad11b92d5a1763ab34ba7b5fc42494bfe08b76d1"
integrity sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==
dependencies:
"@ai-sdk/provider" "1.1.3"
nanoid "^3.3.8"
secure-json-parse "^2.7.0"
"@ai-sdk/provider@1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-1.0.6.tgz#ab7e53a48a2cefe7bd593590a611b2ce12465bc3"
@@ -2047,7 +2064,7 @@
wrap-ansi "^7.0.0"
ws "^8.12.1"
"@expo/code-signing-certificates@^0.0.5":
"@expo/code-signing-certificates@0.0.5", "@expo/code-signing-certificates@^0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz#a693ff684fb20c4725dade4b88a6a9f96b02496c"
integrity sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==
@@ -5131,7 +5148,7 @@ ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.0.0, ajv@^8.9.0:
ajv@^8.0.0, ajv@^8.11.0, ajv@^8.9.0:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
@@ -7379,6 +7396,14 @@ expo-blur@~14.0.1:
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-14.0.3.tgz#656d6b2442bfbbfb2a6608c6bc1151b29bce6698"
integrity sha512-BL3xnqBJbYm3Hg9t/HjNjdeY7N/q8eK5tsLYxswWG1yElISWZmMvrXYekl7XaVCPfyFyz8vQeaxd7q74ZY3Wrw==
expo-build-properties@~0.13.3:
version "0.13.3"
resolved "https://registry.yarnpkg.com/expo-build-properties/-/expo-build-properties-0.13.3.tgz#6b96d0486148fca6e74e62c7c502c0a9990931aa"
integrity sha512-gw7AYP+YF50Gr912BedelRDTfR4GnUEn9p5s25g4nv0hTJGWpBZdCYR5/Oi2rmCHJXxBqhPjxzV7JRh72fntLg==
dependencies:
ajv "^8.11.0"
semver "^7.6.0"
expo-clipboard@~7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/expo-clipboard/-/expo-clipboard-7.0.1.tgz#31d61270e77a37d2a6b7ae9abf79e060497ef43b"
@@ -7433,6 +7458,11 @@ expo-dev-menu@6.0.25:
dependencies:
expo-dev-menu-interface "1.9.3"
expo-eas-client@~0.13.3:
version "0.13.3"
resolved "https://registry.yarnpkg.com/expo-eas-client/-/expo-eas-client-0.13.3.tgz#1535a99a224e360581c6253b0a1ea767e19815b8"
integrity sha512-t+1F1tiDocSot8iSnrn/CjTUMvVvPV2DpafSVcticpbSzMGybEN7wcamO1t18fK7WxGXpZE9gxtd80qwv/LLqQ==
expo-file-system@~18.0.12:
version "18.0.12"
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.0.12.tgz#6ceeeb0725f6c5faaf58112f18c073c2acfb3027"
@@ -7470,7 +7500,7 @@ expo-linking@~7.0.2, expo-linking@~7.0.5:
expo-constants "~17.0.5"
invariant "^2.2.4"
expo-manifests@~0.15.8:
expo-manifests@~0.15.7, expo-manifests@~0.15.8:
version "0.15.8"
resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.15.8.tgz#15e7b7b99d764b40ca3e3f859a126c856e2d6206"
integrity sha512-VuIyaMfRfLZeETNsRohqhy1l7iZ7I+HKMPfZXVL2Yn17TT0WkOhZoq1DzYwPbOHPgp1Uk6phNa86EyaHrD2DLw==
@@ -7544,6 +7574,11 @@ expo-status-bar@~2.0.0:
resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-2.0.1.tgz#fc07726346dc30fbb68aadb0d7890b34fba42eee"
integrity sha512-AkIPX7jWHRPp83UBZ1iXtVvyr0g+DgBVvIXTtlmPtmUsm8Vq9Bb5IGj86PW8osuFlgoTVAg7HI/+Ok7yEYwiRg==
expo-structured-headers@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-4.0.0.tgz#85537ae6daec61ebfb214ede4107c8841c6e16d0"
integrity sha512-uPiwZjWq3AdFGgY52+I2nGPrNa6izxAglymPXHUZLekZW290GqIUOk7MBNDD4sg4JwUbSi3gdxEurpEvuq+FSg==
expo-symbols@~0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/expo-symbols/-/expo-symbols-0.2.2.tgz#753e55ab1fb8b8f3bdb66c0cfd954c390fa7f62b"
@@ -7564,6 +7599,26 @@ expo-updates-interface@~1.0.0:
resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-1.0.0.tgz#b98c66b800d29561c62409556948b2af3d5316e5"
integrity sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ==
expo-updates@~0.27.4:
version "0.27.4"
resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.27.4.tgz#e1c017b285ae5eee1a82b38b10d05a19eef81aa4"
integrity sha512-0rg4L2fFPEjTR/qnZ9Te4Q4irVC8uvNcTZW1pWnWbadG1SLv2PKjS1MYX5BboKzC3ao0H7m++5TP3hWhNg9org==
dependencies:
"@expo/code-signing-certificates" "0.0.5"
"@expo/config" "~10.0.11"
"@expo/config-plugins" "~9.0.17"
"@expo/spawn-async" "^1.7.2"
arg "4.1.0"
chalk "^4.1.2"
expo-eas-client "~0.13.3"
expo-manifests "~0.15.7"
expo-structured-headers "~4.0.0"
expo-updates-interface "~1.0.0"
fast-glob "^3.3.2"
fbemitter "^3.0.0"
ignore "^5.3.1"
resolve-from "^5.0.0"
expo-web-browser@~14.0.1:
version "14.0.2"
resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-14.0.2.tgz#52d53947c42fdfb225e8c230418ffe508bcf98a7"
@@ -8511,6 +8566,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
ignore@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
image-q@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/image-q/-/image-q-4.0.0.tgz#31e075be7bae3c1f42a85c469b4732c358981776"
@@ -11660,23 +11720,35 @@ react-native-helmet-async@2.0.4:
react-fast-compare "^3.2.2"
shallowequal "^1.1.0"
react-native-ios-context-menu@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/react-native-ios-context-menu/-/react-native-ios-context-menu-3.1.0.tgz#8e5b7054959e6d4629d9aa18e5fe51fc18ccc597"
integrity sha512-qdPSXMKUp5lDgmZeUPdv5sgBFhkFrIqma+zsnqJQYOvekb6Qs17yJy1Rqhrj0bJrwuduHzZX0aYbaA8whxqpDw==
react-native-ios-context-menu@3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/react-native-ios-context-menu/-/react-native-ios-context-menu-3.1.3.tgz#ac340cb4052ba7866bca19b365f8425801889139"
integrity sha512-p65JTOxL0D8TOgTgq3A7nVhr/hQuRTtlmsH/aQ7vaOgxY4Na/QVcEF9s4wHc7y+Rcmv84bi6V6DhqxGkFFLPmA==
dependencies:
"@dominicstop/ts-event-emitter" "^1.1.0"
react-native-ios-utilities@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/react-native-ios-utilities/-/react-native-ios-utilities-5.1.2.tgz#78ac76bc07d3464fe746194146c3dea6cff1080a"
integrity sha512-H566MdgC1x0vW6D8EKweno1yRj/gQHeWiK0cRmyRexnUSk7oIecy7l2uyxXLYWFs+yvB/YIdLm+s2c19lKGfaw==
react-native-ios-utilities@5.1.7:
version "5.1.7"
resolved "https://registry.yarnpkg.com/react-native-ios-utilities/-/react-native-ios-utilities-5.1.7.tgz#d9dfbeef45c8598a9fe32f0e2a2e3ff72deae8ec"
integrity sha512-tvJFBfjYHcYFN3PhJOPxU71J+4fBKNWt65FlqxHApVTPcX2xxnrqvSHj7i1qHkjNG7bLDSsTotr+d1gIrXBzQg==
react-native-is-edge-to-edge@^1.1.6:
version "1.1.7"
resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz#28947688f9fafd584e73a4f935ea9603bd9b1939"
integrity sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==
react-native-is-edge-to-edge@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358"
integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==
react-native-keyboard-controller@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.19.0.tgz#c74e8fe2ccc98e5ba15c1dcb659ed5fdb8d3190f"
integrity sha512-G/SPwip7XWObjHLLpMB83pQoki8wQh0dPL4c+uEqxmaDUznnHvkiGuSTz0FWY0UUCeClw/iJ31DirMp6jBC2Jw==
dependencies:
react-native-is-edge-to-edge "^1.2.1"
react-native-mmkv@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-3.2.0.tgz#460723eb23b9cc92c65b0416eae9874a6ddf5b82"