Compare commits

...

19 Commits

Author SHA1 Message Date
Daniel
c7ab767872 Merge pull request #1575 from linkwarden/dev
Dev
2026-01-05 18:09:36 +03:30
daniel31x13
fdf48abd29 add pr template 2026-01-05 09:38:33 -05:00
daniel31x13
59252759f2 revert version number to 0.0.0 in package.json (since we're already tracking the version in app.json) 2026-01-04 16:44:17 -05:00
daniel31x13
dd96d80d42 bump version 2026-01-03 12:24:59 -05:00
daniel31x13
f8efbe95e6 bug fixed 2026-01-03 11:07:01 -05:00
daniel31x13
cf84474921 fix infinite loading bug + enable corepack during eas submit 2025-12-31 08:57:46 -05:00
daniel31x13
eccf27425c fix(mobile): fix infinite spinner bug + fix android dark mode bug 2025-12-29 02:03:37 -05:00
Daniel
3926e566b7 Merge pull request #1556 from linkwarden/dev
v2.13.5
2025-12-28 12:40:56 +03:30
daniel31x13
bca333be26 bump version to v2.13.5 2025-12-28 04:10:24 -05:00
Daniel
02a1e3b455 Merge pull request #1555 from khanguyen74/ai-tagging-fix
Update ai tagging response model
2025-12-28 12:15:25 +03:30
daniel31x13
5b0c66b5e2 minor improvement 2025-12-28 03:44:38 -05:00
Kha Nguyen
7c0c823c41 update ai tagging response model
- llm only needs to return a text containing all the tags
2025-12-26 17:44:07 -06:00
Daniel
a8d2c55d12 Merge pull request #1551 from linkwarden/dev
improved README + add sponsor links
2025-12-26 07:57:58 +03:30
daniel31x13
37410fcf97 improved README + add sponsor links 2025-12-25 23:25:14 -05:00
Daniel
0ab4a2d883 Merge pull request #1548 from linkwarden/dev
v2.13.4
2025-12-26 01:06:45 +03:30
Daniel
756b896fe6 New Crowdin updates (#1542)
* New translations common.json (Japanese)

* New translations common.json (Japanese)

* New translations common.json (Japanese)

* New translations common.json (Italian)

---------

Co-authored-by: LinkwardenBot <bot@linkwarden.app>
2025-12-25 16:36:01 -05:00
daniel31x13
e3e3611b54 bump version 2025-12-25 16:31:14 -05:00
Daniel
4bf65f8ebd Merge pull request #1547 from khanguyen74/fix-announcement
fix new version announcement keeps showing
2025-12-26 00:59:13 +03:30
Kha Nguyen
6956c71aa2 fix new version announcement keeps showing 2025-12-25 13:20:17 -06:00
28 changed files with 296 additions and 231 deletions

14
.github/FUNDING.yml vendored
View File

@@ -1,13 +1,3 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: linkwarden
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
github: daniel31x13
buy_me_a_coffee: daniel31x13

46
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,46 @@
## What does this PR do?
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
- Fixes #XXXX (GitHub issue number)
## Visual Demo
A visual demonstration is strongly recommended, for both the original and new change **(video / image)**.
#### Video Demo (if applicable):
- Show screen recordings of the issue or feature.
- Demonstrate how to reproduce the issue, the behavior before and after the change.
#### Image Demo (if applicable):
- Add side-by-side screenshots of the original and updated change.
- Highlight any significant change(s).
## AI Assistance (Required)
We allow AI-assisted development, but reviewers need transparency to assess risk, maintainability, and correctness.
#### AI usage level (check one)
- [ ] None (no AI used)
- [ ] Light (spellcheck/rewording/comments/docs only)
- [ ] Medium (AI suggested small code changes/snippets that I adapted)
- [ ] Heavy (AI significantly shaped the implementation or architecture)
#### Which tool(s) where used?
- e.g., ChatGPT, Copilot, Cursor, etc.
## What was verified by the author?
<!-- Add what you personally checked to ensure correctness and safety. -->
- [ ] I reviewed **and** understood all AI/human generated code
- [ ] I validated behavior locally (tests/manual verification)
- [ ] I checked edge cases and failure modes
## Submission Acknowledgement
- [ ] I acknowledge that a decent size PR without self-review might be rejected

View File

@@ -38,32 +38,49 @@ Linkwarden is also designed with collaboration in mind, enabling you to share li
## Features
- 📸 Auto capture a screenshot, PDF, and single html file of each webpage.
- 📖 Reader view of the webpage, with the ability to highlight and annotate text.
- 🏛️ Send your webpage to Wayback Machine ([archive.org](https://archive.org)) for a snapshot. (Optional)
- ✨ Local AI Tagging to automatically tag your links based on their content (Optional).
- 📂 Organize links by collection, sub-collection, name, description and multiple tags.
- 👥 Collaborate on gathering links in a collection.
- 🎛️ Customize the permissions of each member.
- 🌐 Share your collected links and preserved formats with the world.
- 📌 Pin your favorite links to dashboard.
- 🔍 Full text search, filter and sort for easy retrieval.
- 📱 Responsive design and supports most modern browsers.
- 🌓 Dark/Light mode support.
- 🧩 Browser extension. [Star it here!](https://github.com/linkwarden/browser-extension)
- 📸 Auto capture a screenshot, PDF, and single html file of each webpage
- 📖 Reader view of the webpage, with the ability to highlight and annotate text
- 🏛️ Send your webpage to Wayback Machine ([archive.org](https://archive.org)) for a snapshot (optional)
- ✨ Local AI Tagging to automatically tag your links based on their content (optional)
- 📂 Organize links by collection, sub-collection, name, description and multiple tags
- 👥 Collaborate on gathering links in a collection
- 🎛️ Customize the permissions of each member
- 🌐 Share your collected links and preserved formats with the world
- 📱 Native iOS and android mobile apps
- 🔍 Full text search, filter and sort for easy retrieval
- 🌓 Dark/Light mode support
- 🧩 Browser extension (star it [here](https://github.com/linkwarden/browser-extension)!)
- 🔄 Browser Synchronization (using [Floccus](https://floccus.org)!)
- Import and export your bookmarks.
- 🔐 SSO integration. (Enterprise and Self-hosted users only)
- 📦 Installable Progressive Web App (PWA).
- 🍎 iOS Shortcut to save Links to Linkwarden.
- 🔑 API keys.
- ✅ Bulk actions.
- 👥 User administration.
- 🌐 Support for Other Languages (i18n).
- 📁 Image and PDF Uploads.
- 🎨 Custom Icons for Links and Collections.
- 🔔 RSS Feed Subscription.
- ✨ And many more features. (Literally!)
- Upload from SingleFile (check out the [guide](https://docs.linkwarden.app/Usage/upload-from-singlefile))
- 🔐 SSO integration (Enterprise and Self-hosted users only)
- 🍎 iOS Shortcut to save links to Linkwarden
- 🔑 API keys
- ✅ Bulk actions
- 👥 User administration
- 🌐 Support for other languages (i18n)
- 📁 Image and PDF uploads
- 🎨 Custom icons for links and collections
- 🔔 RSS feed subscription
- ✨ And many more features (literally!)
## Get Our Official Mobile App
<img src="./assets/mobile_apps.png" alt="Different screens (iPad, Pixel, and iPhone)" width="400" />
> [!IMPORTANT]
> To use the app youll first need a Linkwarden account.
To create an account, you can choose between:
- [**Linkwarden Cloud**](https://linkwarden.app/#pricing) instant setup, and your subscription directly supports ongoing development.
- [**Self-hosted Linkwarden**](https://docs.linkwarden.app/self-hosting/installation) free, but youll need to deploy and maintain a Linkwarden instance on a server.
After creating an account, download the app from your preferred store:
[![Download on the App Store](./assets/app_store.png)](https://apps.apple.com/app/linkwarden/id6752550960)
[![Get it on Google Play](./assets/google_play.png)](https://play.google.com/store/apps/details?id=app.linkwarden)
(To get the app as an APK outside Google Play, check out our [builds](https://github.com/linkwarden/builds) repository.)
## Like what we're doing? Give us a Star ⭐

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Linkwarden",
"slug": "linkwarden",
"version": "1.0.0",
"version": "1.0.1",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "linkwarden",

View File

@@ -44,7 +44,7 @@ export default function CollectionsScreen() {
collapsableChildren={false}
>
{collections.isLoading ? (
<View className="flex justify-center h-full items-center">
<View className="flex justify-center h-screen items-center">
<ActivityIndicator size="large" />
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
</View>

View File

@@ -1,4 +1,11 @@
import { Platform, ScrollView, StyleSheet } from "react-native";
import {
ActivityIndicator,
Platform,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useEffect, useMemo, useState } from "react";
import { useDashboardData } from "@linkwarden/router/dashboardData";
import useAuthStore from "@/store/auth";
@@ -53,22 +60,36 @@ export default function DashboardScreen() {
});
}, [dashboardSections]);
const [pullRefreshing, setPullRefreshing] = useState(false);
const onRefresh = async () => {
setPullRefreshing(true);
try {
await Promise.all([
dashboardData.refetch(),
userData.refetch(),
collectionsData.refetch(),
tagsData.refetch(),
]);
} finally {
setPullRefreshing(false);
}
};
if (orderedSections.length === 0 && dashboardData.isLoading)
return (
<View className="flex justify-center h-screen items-center bg-base-100">
<ActivityIndicator size="large" />
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
</View>
);
return (
<ScrollView
refreshControl={
<Spinner
refreshing={
dashboardData.isRefetching ||
userData.isRefetching ||
collectionsData.isRefetching ||
tagsData.isRefetching
}
onRefresh={() => {
dashboardData.refetch();
userData.refetch();
collectionsData.refetch();
tagsData.refetch();
}}
refreshing={pullRefreshing}
onRefresh={onRefresh}
progressBackgroundColor={
rawTheme[colorScheme as ThemeName]["base-200"]
}

View File

@@ -42,7 +42,7 @@ export default function TagsScreen() {
collapsableChildren={false}
>
{tags.isLoading ? (
<View className="flex justify-center h-full items-center">
<View className="flex justify-center h-screen items-center">
<ActivityIndicator size="large" />
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
</View>

View File

@@ -20,8 +20,9 @@ export default function HomeScreen() {
return (
<Animated.View
entering={SlideInDown.springify().damping(100).stiffness(300)}
className="flex-col justify-end h-full bg-primary relative"
className="flex-col justify-end h-full"
>
<View className="h-full bg-primary relative">
<View className="my-auto">
<Image
source={require("@/assets/images/linkwarden.png")}
@@ -70,6 +71,7 @@ export default function HomeScreen() {
<Text className="text-neutral text-center w-fit">Need help?</Text>
</TouchableOpacity>
</SafeAreaView>
</View>
</Animated.View>
);
}

View File

@@ -1,4 +1,4 @@
import { Alert, Platform, Text, View } from "react-native";
import { Alert, Text, View } from "react-native";
import { useRef, useState } from "react";
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
import Input from "@/components/ui/Input";

View File

@@ -1,4 +1,4 @@
import { View, Text, Alert, Platform } from "react-native";
import { View, Text, Alert } from "react-native";
import { useCallback, useEffect, useMemo, useState } from "react";
import ActionSheet, {
FlatList,

View File

@@ -1,4 +1,4 @@
import { Alert, Platform, Text, View } from "react-native";
import { Alert, Text, View } from "react-native";
import { useRef, useState } from "react";
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
import Input from "@/components/ui/Input";

View File

@@ -28,7 +28,7 @@ export default function Links({ links, data }: Props) {
const [promptedRefetch, setPromptedRefetch] = useState(false);
return data.isLoading ? (
<View className="flex justify-center h-full items-center">
<View className="flex justify-center h-screen items-center">
<ActivityIndicator size="large" />
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
</View>

View File

@@ -1,17 +0,0 @@
import { PropsWithChildren } from "react";
import { IconSymbol } from "../ui/IconSymbol";
import ModalBase from "../ModalBase";
import { Text } from "react-native";
type Props = PropsWithChildren<{
isVisible: boolean;
onClose: () => void;
}>;
export default function AddLink({ isVisible, onClose }: Props) {
return (
// <ModalBase isVisible={isVisible} onClose={onClose}>
<Text>Hi</Text>
// </ModalBase>
);
}

View File

@@ -20,6 +20,7 @@
}
},
"production": {
"corepack": true,
"distribution": "store",
"autoIncrement": true,
"channel": "production"

View File

@@ -1,7 +1,7 @@
{
"name": "@linkwarden/mobile",
"main": "expo-router/entry",
"version": "1.0.0",
"version": "0.0.0",
"scripts": {
"start": "expo start",
"android": "expo run:android",

View File

@@ -3,13 +3,10 @@ import * as SecureStore from "expo-secure-store";
import { router } from "expo-router";
import { MobileAuth } from "@linkwarden/types";
import { Alert } from "react-native";
import * as FileSystem from "expo-file-system";
import { queryClient } from "@/lib/queryClient";
import { mmkvPersister } from "@/lib/queryPersister";
import { clearCache } from "@/lib/cache";
const CACHE_DIR = FileSystem.documentDirectory + "archivedData/";
type AuthStore = {
auth: MobileAuth;
signIn: (
@@ -78,31 +75,42 @@ const useAuthStore = create<AuthStore>((set) => ({
}
});
} else {
await fetch(instance + "/api/v1/session", {
try {
const res = await Promise.race([
fetch(`${instance}/api/v1/session`, {
method: "POST",
body: JSON.stringify({ username, password }),
headers: {
"Content-Type": "application/json",
},
}).then(async (res) => {
headers: { "Content-Type": "application/json" },
}),
new Promise<Response>((_, reject) =>
setTimeout(() => reject(new Error("TIMEOUT")), 30000)
),
]);
if (res.ok) {
const data = await res.json();
const session = (data as any).response.token;
await SecureStore.setItemAsync("TOKEN", session);
await SecureStore.setItemAsync("INSTANCE", instance);
set({
auth: {
session,
instance,
status: "authenticated",
},
});
set({ auth: { session, instance, status: "authenticated" } });
router.replace("/(tabs)/dashboard");
} else {
Alert.alert("Error", "Invalid credentials");
}
});
} catch (err: any) {
if (err?.message === "TIMEOUT") {
Alert.alert(
"Request timed out",
"Unable to reach the server in time. Please check your network configuration and try again."
);
} else {
Alert.alert(
"Network error",
"Could not connect to the server. Please check your network configuration and try again."
);
}
}
}
},
signOut: async () => {

View File

@@ -15,13 +15,13 @@ const useDataStore = create<DataStore>((set, get) => ({
hasShareIntent: false,
url: "",
},
theme: "light",
theme: "system",
preferredBrowser: "app",
},
setData: async () => {
const dataString = JSON.parse((await AsyncStorage.getItem("data")) || "{}");
colorScheme.set(dataString.theme || "light");
colorScheme.set(dataString.theme || "system");
if (dataString)
set((state) => ({ data: { ...state.data, ...dataString } }));

View File

@@ -12,8 +12,8 @@ export default async function getLatestVersion(setShowAnnouncement: Function) {
const latestMessage = data.message;
if (
announcementId !== latestAnnouncement ||
announcementMessage !== latestMessage
announcementId != latestAnnouncement ||
announcementMessage != latestMessage
) {
setShowAnnouncement(true);
if (latestAnnouncement)

View File

@@ -1,6 +1,6 @@
{
"name": "@linkwarden/web",
"version": "v2.13.3",
"version": "v2.13.5",
"main": "index.js",
"repository": "https://github.com/linkwarden/linkwarden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>",

View File

@@ -393,75 +393,75 @@
"saved": "Salvato",
"untitled": "Senza titolo",
"no_tags": "Nessun tag.",
"no_description_provided": "No description provided.",
"change_icon": "Change Icon",
"upload_banner": "Upload Banner",
"columns": "Columns",
"no_description_provided": "Nessuna descrizione fornita.",
"change_icon": "Cambia Icona",
"upload_banner": "Carica Grafica",
"columns": "Colonne",
"default": "Predefinito",
"invalid_url_guide": "Please enter a valid Address for the Link. (It should start with http/https)",
"email_invalid": "Please enter a valid email address.",
"username_invalid_guide": "Username has to be at least 3 characters, no spaces and special characters are allowed.",
"team_management": "Team Management",
"invite_user": "Invite User",
"invite_users": "Invite Users",
"invite_user_desc": "To invite someone to your team, please enter their email address below:",
"invalid_url_guide": "Inserisci un Indirizzo valido per il Collegamento. (Dovrebbe iniziare con http/https)",
"email_invalid": "Inserisci un indirizzo email valido.",
"username_invalid_guide": "Il nome utente deve contenere almeno 3 caratteri, non sono ammessi spazi e caratteri speciali.",
"team_management": "Gestione Team",
"invite_user": "Invita Utente",
"invite_users": "Invita Utenti",
"invite_user_desc": "Per invitare qualcuno nel tuo team, inserisci il suo indirizzo email qui sotto:",
"invite_user_note": "Please note that once the invitation is accepted, an additional seat will be purchased and your account will automatically be billed for this addition.",
"invite_user_price": "The cost of each seat is ${{price}} per month or ${{priceAnnual}} per year, depending on your current subscription plan.",
"send_invitation": "Send Invitation",
"learn_more": "Learn more",
"invite_user_price": "Il costo di ogni postazione è di ${{price}} al mese o ${{priceAnnual}} all'anno, a seconda del tuo attuale piano di abbonamento.",
"send_invitation": "Invia Invito",
"learn_more": "Ulteriori informazioni",
"invitation_desc": "{{owner}} ti ha invitato a unirti a Linkwarden. \nPer continuare, per favore completa la configurazione del tuo account.",
"invitation_accepted": "Invitation Accepted!",
"status": "Status",
"pending": "Pending",
"active": "Active",
"manage_seats": "Manage Seats",
"invitation_accepted": "Invito Accettato!",
"status": "Stato",
"pending": "In Attesa",
"active": "Attivo",
"manage_seats": "Gestisci Postazioni",
"seats_purchased": "{{count}} posti acquistati",
"seat_purchased": "{{count}} seat purchased",
"date_added": "Date Added",
"resend_invite": "Resend Invitation",
"resend_invite_success": "Invitation Resent!",
"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.",
"click_out_to_apply": "Click outside to apply",
"submit": "Submit",
"thanks_for_feedback": "Thanks for your feedback!",
"quick_survey": "Quick Survey",
"seat_purchased": "{{count}} postazioni acquistate",
"date_added": "Data Aggiunta",
"resend_invite": " Reinvia Invito",
"resend_invite_success": "Invito Reinviato!",
"remove_user": "Rimuovi Utente",
"continue_to_dashboard": "Continua alla Dashboard",
"confirm_user_removal_desc": "Dovranno sottoscrivere un abbonamento per accedere nuovamente a Linkwarden.",
"click_out_to_apply": "Fai clic all'esterno per applicare",
"submit": "Invia",
"thanks_for_feedback": "Grazie per il tuo feedback!",
"quick_survey": "Sondaggio Rapido",
"how_did_you_discover_linkwarden": "Come hai scoperto Linkwarden?",
"rather_not_say": "Rather not say",
"search_engine": "Search Engine (Google, Bing, etc.)",
"rather_not_say": "Preferisco non specificarlo",
"search_engine": "Motore di Ricerca (Google, Bing, ecc.)",
"reddit": "Reddit",
"lemmy": "Lemmy",
"people_recommendation": "Recommendation (Friend, Family, etc.)",
"open_all_links": "Open all Links",
"people_recommendation": "Raccomandazione (Amico, Famiglia, etc.)",
"open_all_links": "Apri tutti i Link",
"ai_settings": "Impostazioni IA",
"generate_tags_for_existing_links": "Generate tags for existing Links",
"generate_tags_for_existing_links": "Genera tag per i Link esistenti",
"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_predefined_tags_desc": "Categorizza automaticamente i link in tag predefiniti in base al contenuto di ciascun 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.",
"based_on_existing_tags_desc": "Categorizza automaticamente i link in tag esistenti in base al contenuto di ciascun link.",
"auto_generate_tags": "Auto-generate Tags",
"auto_generate_tags_desc": "Auto-generate relevant tags based on the content of each link.",
"auto_generate_tags_desc": "Genera automaticamente tag pertinenti in base al contenuto di ogni link.",
"disabled": "Disabilitato",
"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_subscriptions_desc": "Le iscrizioni ai feed RSS sono un modo per rimanere aggiornati sui tuoi siti web e blog preferiti. Linkwarden recupererà automaticamente gli ultimi articoli ogni {{number}} minuti dai feed da te forniti.",
"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": "Feed RSS",
"pinned_links": "Pinned Links",
"recent_links": "Recent Links",
"pinned_links": "Link Fissati",
"recent_links": "Link Recenti",
"search_results": "Search Results",
"linkwarden_icon": "Icona Linkwarden",
"permanent_session": "This is a permanent session",
"locale": "en-US",
"not_found_404": "404 - Not Found",
"collection_publicly_shared": "Questa collezione è condivisa pubblicamente.",
"search_for_links": "Search for Links",
"search_for_links": "Cerca per Link",
"search_query_invalid_symbol": "The search query should not contain '%'.",
"open_modal_new_tab": "Open this modal in a new tab",
"file": "File",

View File

@@ -494,36 +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",
"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.",
"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."
"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

@@ -5,7 +5,7 @@ import {
predefinedTagsPrompt,
} from "./prompts";
import { prisma } from "@linkwarden/prisma";
import { generateObject } from "ai";
import { generateText } from "ai";
import { LanguageModelV2 } from "@ai-sdk/provider";
import {
createOpenAICompatible,
@@ -13,7 +13,6 @@ import {
} 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";
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { createOllama } from "ollama-ai-provider-v2";
@@ -127,15 +126,13 @@ export default async function autoTagLink(
return console.log("No predefined tags to auto tag for link: ", link.url);
}
const { object } = await generateObject({
const { text } = await generateText({
model: getAIModel(),
prompt: prompt,
output: "array",
schema: z.string(),
});
try {
let tags = object;
let tags: string[] = JSON.parse(text);
if (!tags || tags.length === 0) {
return;

View File

@@ -1,6 +1,6 @@
export const generateTagsPrompt = (text: string) => `
You are a Bookmark Manager that should extract relevant tags from the following text, here are the rules:
- The final output should be only an array of tags (like ["tag1", "tag2", "...").
- The final output should be only an array of tags (like ["tag1", "tag2", "..."]).
- The tags should be in the language of the text.
- The maximum number of tags is 5.
- Each tag should be maximum one to two words.
@@ -15,7 +15,7 @@ export const predefinedTagsPrompt = (text: string, tags: string[]) => `
You are a Bookmark Manager that should match the following text with predefined tags.
Predefined tags: ${tags.join(", ")}.
Here are the rules:
- The final output should be only an array of tags (like ["tag1", "tag2", "...").
- The final output should be only an array of tags (like ["tag1", "tag2", "..."]).
- The tags should be in the language of the text.
- The maximum number of tags is 5.
- Each tag should be maximum one to two words.
@@ -30,7 +30,7 @@ export const existingTagsPrompt = (text: string, tags: string[]) => `
You are a Bookmark Manager that should match the following text with existing tags.
The existing tags are sorted from most used to least used: ${tags.join(", ")}.
Here are the rules:
- The final output should be only an array of tags (like ["tag1", "tag2", "...").
- The final output should be only an array of tags (like ["tag1", "tag2", "..."]).
- The tags should be in the language of the text.
- The maximum number of tags is 5.
- Each tag should be maximum one to two words.

BIN
assets/app_store.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
assets/google_play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 KiB

After

Width:  |  Height:  |  Size: 606 KiB

BIN
assets/mobile_apps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB