Compare commits

...

1428 Commits
v2.6.1 ... main

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
Daniel
e0f357513c Merge pull request #1541 from linkwarden/dev
Remove unnecessary .yarn copy from Dockerfile
2025-12-22 20:18:35 -05:00
daniel31x13
daeb859990 Remove unnecessary .yarn copy from Dockerfile 2025-12-22 20:17:58 -05:00
Daniel
f072bcd0b0 Merge pull request #1540 from linkwarden/dev
v2.13.3
2025-12-22 18:51:06 -05:00
daniel31x13
a497dc953a Add PRISMA_HIDE_UPDATE_MESSAGE environment variable to Dockerfile 2025-12-22 18:17:25 -05:00
daniel31x13
dcf6d72c01 bug fix 2025-12-22 17:36:09 -05:00
daniel31x13
d749487fb6 Add meili_data to .dockerignore and .gitignore 2025-12-22 17:21:18 -05:00
Daniel
7079355013 Merge pull request #1537 from linkwarden/chore/tech-debts
Chore/tech debts
2025-12-21 18:16:27 -05:00
daniel31x13
46762b0d36 minor change 2025-12-21 18:10:31 -05:00
Daniel
389e5df117 Chore/tech debts (#1536)
* build(deps): bump the npm_and_yarn group across 5 directories with 22 updates

Bumps the npm_and_yarn group with 18 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [axios](https://github.com/axios/axios) | `1.5.1` | `1.12.0` |
| [dompurify](https://github.com/cure53/DOMPurify) | `3.0.6` | `3.2.4` |
| [formidable](https://github.com/node-formidable/formidable) | `3.5.1` | `3.5.4` |
| [next](https://github.com/vercel/next.js) | `13.4.12` | `14.2.35` |
| [next-auth](https://github.com/nextauthjs/next-auth) | `4.22.1` | `4.24.12` |
| [playwright](https://github.com/microsoft/playwright) | `1.55.0` | `1.55.1` |
| [@mozilla/readability](https://github.com/mozilla/readability) | `0.4.4` | `0.6.0` |
| [ai](https://github.com/vercel/ai) | `4.3.9` | `5.0.52` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `6.9.3` | `7.0.11` |
| [brace-expansion](https://github.com/juliangruber/brace-expansion) | `1.1.11` | `1.1.12` |
| [braces](https://github.com/micromatch/braces) | `3.0.2` | `3.0.3` |
| [form-data](https://github.com/form-data/form-data) | `3.0.3` | `3.0.4` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `3.14.1` | `3.14.2` |
| [micromatch](https://github.com/micromatch/micromatch) | `4.0.5` | `4.0.8` |
| [min-document](https://github.com/Raynos/min-document) | `2.19.0` | `2.19.2` |
| [nanoid](https://github.com/ai/nanoid) | `3.3.6` | `3.3.8` |
| [node-forge](https://github.com/digitalbazaar/forge) | `1.3.1` | `1.3.3` |
| [tar](https://github.com/isaacs/node-tar) | `6.1.13` | `6.2.1` |

Bumps the npm_and_yarn group with 1 update in the /apps/web directory: [next](https://github.com/vercel/next.js).
Bumps the npm_and_yarn group with 2 updates in the /apps/worker directory: [@mozilla/readability](https://github.com/mozilla/readability) and [ai](https://github.com/vercel/ai).
Bumps the npm_and_yarn group with 1 update in the /packages/lib directory: [nodemailer](https://github.com/nodemailer/nodemailer).
Bumps the npm_and_yarn group with 1 update in the /packages/router directory: [next](https://github.com/vercel/next.js).


Updates `axios` from 1.5.1 to 1.12.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.5.1...v1.12.0)

Updates `dompurify` from 3.0.6 to 3.2.4
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.0.6...3.2.4)

Updates `formidable` from 3.5.1 to 3.5.4
- [Release notes](https://github.com/node-formidable/formidable/releases)
- [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-formidable/formidable/commits)

Updates `next` from 13.4.12 to 14.2.35
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.12...v14.2.35)

Updates `next-auth` from 4.22.1 to 4.24.12
- [Release notes](https://github.com/nextauthjs/next-auth/releases)
- [Commits](https://github.com/nextauthjs/next-auth/compare/next-auth@4.22.1...next-auth@4.24.12)

Updates `playwright` from 1.55.0 to 1.55.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.55.0...v1.55.1)

Updates `postcss` from 8.4.26 to 8.5.3
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.26...8.5.3)

Updates `@mozilla/readability` from 0.4.4 to 0.6.0
- [Changelog](https://github.com/mozilla/readability/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mozilla/readability/compare/0.4.4...0.6.0)

Updates `ai` from 4.3.9 to 5.0.52
- [Release notes](https://github.com/vercel/ai/releases)
- [Changelog](https://github.com/vercel/ai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vercel/ai/compare/ai@4.3.9...ai@5.0.52)

Updates `nodemailer` from 6.9.3 to 7.0.11
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.3...v7.0.11)

Updates `@babel/runtime` from 7.21.5 to 7.27.0
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.27.0/packages/babel-runtime)

Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `follow-redirects` from 1.15.3 to 1.15.11
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.11)

Updates `form-data` from 3.0.3 to 3.0.4
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v3.0.3...v3.0.4)

Updates `jose` from 4.14.4 to 4.15.9
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/v4.15.9/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v4.14.4...v4.15.9)

Updates `js-yaml` from 3.14.1 to 3.14.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

Updates `micromatch` from 4.0.5 to 4.0.8
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

Updates `min-document` from 2.19.0 to 2.19.2
- [Commits](https://github.com/Raynos/min-document/compare/v2.19.0...v2.19.2)

Updates `nanoid` from 3.3.6 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.6...3.3.8)

Updates `node-forge` from 1.3.1 to 1.3.3
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.3)

Updates `tar` from 6.1.13 to 6.2.1
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.1.13...v6.2.1)

Updates `next` from 13.4.12 to 14.2.35
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.12...v14.2.35)

Updates `@mozilla/readability` from 0.4.4 to 0.6.0
- [Changelog](https://github.com/mozilla/readability/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mozilla/readability/compare/0.4.4...0.6.0)

Updates `ai` from 4.3.19 to 5.0.113
- [Release notes](https://github.com/vercel/ai/releases)
- [Changelog](https://github.com/vercel/ai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vercel/ai/compare/ai@4.3.9...ai@5.0.52)

Updates `nodemailer` from 6.10.1 to 7.0.11
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.3...v7.0.11)

Updates `next` from 13.4.12 to 14.2.35
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.12...v14.2.35)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 3.2.4
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: formidable
  dependency-version: 3.5.4
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.35
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next-auth
  dependency-version: 4.24.12
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: playwright
  dependency-version: 1.55.1
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.3
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@mozilla/readability"
  dependency-version: 0.6.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: ai
  dependency-version: 5.0.52
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: nodemailer
  dependency-version: 7.0.11
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@babel/runtime"
  dependency-version: 7.27.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: braces
  dependency-version: 3.0.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: follow-redirects
  dependency-version: 1.15.11
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: form-data
  dependency-version: 3.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: jose
  dependency-version: 4.15.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: micromatch
  dependency-version: 4.0.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: min-document
  dependency-version: 2.19.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-version: 3.3.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: node-forge
  dependency-version: 1.3.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: tar
  dependency-version: 6.2.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.35
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@mozilla/readability"
  dependency-version: 0.6.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: ai
  dependency-version: 5.0.113
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: nodemailer
  dependency-version: 7.0.11
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.35
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>

* bug fixes and improvements

* always show navbar in reader view

* bug fix and small performance improvement

* minor fix

* Refactor link selection management and bulk actions

- Replaced the use of selectedLinks with selectedIds in the link store for better performance and clarity.
- Updated LinkListOptions, BulkDeleteLinksModal, and BulkEditLinksModal components to utilize the new selection management.
- Modified LinkCard, LinkMasonry, and LinkList components to handle selection state through props.
- Enhanced updateLinks API to support bulk updates with improved tag management.
- Cleaned up unused imports and code related to previous selection methods.

* move refetching logic to Links component

* move disableDraggable and user hook out of each card to improve efficiency

* cleaner code

* memoize components and increase performance

* fix: update announcement links to use the correct domain

* feat: add favicon field to Link model + update packages + bug fix

* feat: implement favicon fetching API and update Link model for favicon support

* feat: add priority attribute to Image components in Sidebar

* Refactor pages to use consistent layout handling (yes, I forgot to do that until now :P)

* bump version

* Refactor setting pages to use consistent layout handling

* upgrade yarn to 4.12.0

* fix DnD bug

* Enhance announcement handling by adding support for announcement messages

* slimmed down the docker image size

* update Node and yarn versions in playwright tests workflow

* small fix

* fix attempt 2

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-21 18:09:05 -05:00
daniel31x13
865bff2214 fix attempt 2 2025-12-21 17:58:30 -05:00
daniel31x13
33553e22d5 small fix 2025-12-21 17:52:44 -05:00
daniel31x13
1323787294 update Node and yarn versions in playwright tests workflow 2025-12-21 17:46:20 -05:00
Daniel
0a812fa72b Merge branch 'dev' into chore/tech-debts 2025-12-21 17:41:05 -05:00
Daniel
9faf9d844e New Crowdin updates (#1535)
* New translations common.json (Chinese Traditional)

* New translations common.json (Chinese Traditional)

---------

Co-authored-by: LinkwardenBot <bot@linkwarden.app>
2025-12-21 17:39:31 -05:00
daniel31x13
1558854f78 slimmed down the docker image size 2025-12-21 10:26:23 -05:00
daniel31x13
ab19df767c Enhance announcement handling by adding support for announcement messages 2025-12-20 15:33:52 -05:00
daniel31x13
218fd504bf fix DnD bug 2025-12-20 15:06:32 -05:00
daniel31x13
02158d7621 upgrade yarn to 4.12.0 2025-12-20 14:05:28 -05:00
daniel31x13
9edb42d181 Refactor setting pages to use consistent layout handling 2025-12-19 05:50:59 -05:00
daniel31x13
8ba370bf62 bump version 2025-12-19 05:29:00 -05:00
daniel31x13
a32934ee9d Refactor pages to use consistent layout handling (yes, I forgot to do that until now :P) 2025-12-19 04:59:32 -05:00
daniel31x13
ff5ba2097d feat: add priority attribute to Image components in Sidebar 2025-12-19 03:14:49 -05:00
daniel31x13
703f84403e feat: implement favicon fetching API and update Link model for favicon support 2025-12-17 02:38:54 -05:00
daniel31x13
f9ec18c51a feat: add favicon field to Link model + update packages + bug fix 2025-12-16 02:23:36 -05:00
daniel31x13
3d0651d4af fix: update announcement links to use the correct domain 2025-12-16 01:13:00 -05:00
daniel31x13
f30bf63c24 memoize components and increase performance 2025-12-15 13:08:58 -05:00
daniel31x13
e2d89a56d6 cleaner code 2025-12-14 22:31:01 -05:00
daniel31x13
40c3ccca93 move disableDraggable and user hook out of each card to improve efficiency 2025-12-14 18:20:57 -05:00
daniel31x13
1e515d5284 move refetching logic to Links component 2025-12-14 17:19:49 -05:00
daniel31x13
cb9cdc92c8 Refactor link selection management and bulk actions
- Replaced the use of selectedLinks with selectedIds in the link store for better performance and clarity.
- Updated LinkListOptions, BulkDeleteLinksModal, and BulkEditLinksModal components to utilize the new selection management.
- Modified LinkCard, LinkMasonry, and LinkList components to handle selection state through props.
- Enhanced updateLinks API to support bulk updates with improved tag management.
- Cleaned up unused imports and code related to previous selection methods.
2025-12-14 11:57:27 -05:00
daniel31x13
639f777b8a minor fix 2025-12-14 01:57:29 -05:00
daniel31x13
48b7384490 bug fix and small performance improvement 2025-12-14 01:51:50 -05:00
daniel31x13
3bff1650c7 always show navbar in reader view 2025-12-13 22:43:34 -05:00
daniel31x13
cadea5c654 bug fixes and improvements 2025-12-13 22:29:36 -05:00
dependabot[bot]
e9b7c21ea0 build(deps): bump the npm_and_yarn group across 5 directories with 22 updates
Bumps the npm_and_yarn group with 18 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [axios](https://github.com/axios/axios) | `1.5.1` | `1.12.0` |
| [dompurify](https://github.com/cure53/DOMPurify) | `3.0.6` | `3.2.4` |
| [formidable](https://github.com/node-formidable/formidable) | `3.5.1` | `3.5.4` |
| [next](https://github.com/vercel/next.js) | `13.4.12` | `14.2.35` |
| [next-auth](https://github.com/nextauthjs/next-auth) | `4.22.1` | `4.24.12` |
| [playwright](https://github.com/microsoft/playwright) | `1.55.0` | `1.55.1` |
| [@mozilla/readability](https://github.com/mozilla/readability) | `0.4.4` | `0.6.0` |
| [ai](https://github.com/vercel/ai) | `4.3.9` | `5.0.52` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `6.9.3` | `7.0.11` |
| [brace-expansion](https://github.com/juliangruber/brace-expansion) | `1.1.11` | `1.1.12` |
| [braces](https://github.com/micromatch/braces) | `3.0.2` | `3.0.3` |
| [form-data](https://github.com/form-data/form-data) | `3.0.3` | `3.0.4` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `3.14.1` | `3.14.2` |
| [micromatch](https://github.com/micromatch/micromatch) | `4.0.5` | `4.0.8` |
| [min-document](https://github.com/Raynos/min-document) | `2.19.0` | `2.19.2` |
| [nanoid](https://github.com/ai/nanoid) | `3.3.6` | `3.3.8` |
| [node-forge](https://github.com/digitalbazaar/forge) | `1.3.1` | `1.3.3` |
| [tar](https://github.com/isaacs/node-tar) | `6.1.13` | `6.2.1` |

Bumps the npm_and_yarn group with 1 update in the /apps/web directory: [next](https://github.com/vercel/next.js).
Bumps the npm_and_yarn group with 2 updates in the /apps/worker directory: [@mozilla/readability](https://github.com/mozilla/readability) and [ai](https://github.com/vercel/ai).
Bumps the npm_and_yarn group with 1 update in the /packages/lib directory: [nodemailer](https://github.com/nodemailer/nodemailer).
Bumps the npm_and_yarn group with 1 update in the /packages/router directory: [next](https://github.com/vercel/next.js).


Updates `axios` from 1.5.1 to 1.12.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.5.1...v1.12.0)

Updates `dompurify` from 3.0.6 to 3.2.4
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.0.6...3.2.4)

Updates `formidable` from 3.5.1 to 3.5.4
- [Release notes](https://github.com/node-formidable/formidable/releases)
- [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-formidable/formidable/commits)

Updates `next` from 13.4.12 to 14.2.35
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.12...v14.2.35)

Updates `next-auth` from 4.22.1 to 4.24.12
- [Release notes](https://github.com/nextauthjs/next-auth/releases)
- [Commits](https://github.com/nextauthjs/next-auth/compare/next-auth@4.22.1...next-auth@4.24.12)

Updates `playwright` from 1.55.0 to 1.55.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.55.0...v1.55.1)

Updates `postcss` from 8.4.26 to 8.5.3
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.26...8.5.3)

Updates `@mozilla/readability` from 0.4.4 to 0.6.0
- [Changelog](https://github.com/mozilla/readability/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mozilla/readability/compare/0.4.4...0.6.0)

Updates `ai` from 4.3.9 to 5.0.52
- [Release notes](https://github.com/vercel/ai/releases)
- [Changelog](https://github.com/vercel/ai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vercel/ai/compare/ai@4.3.9...ai@5.0.52)

Updates `nodemailer` from 6.9.3 to 7.0.11
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.3...v7.0.11)

Updates `@babel/runtime` from 7.21.5 to 7.27.0
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.27.0/packages/babel-runtime)

Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `follow-redirects` from 1.15.3 to 1.15.11
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.11)

Updates `form-data` from 3.0.3 to 3.0.4
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v3.0.3...v3.0.4)

Updates `jose` from 4.14.4 to 4.15.9
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/v4.15.9/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v4.14.4...v4.15.9)

Updates `js-yaml` from 3.14.1 to 3.14.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

Updates `micromatch` from 4.0.5 to 4.0.8
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

Updates `min-document` from 2.19.0 to 2.19.2
- [Commits](https://github.com/Raynos/min-document/compare/v2.19.0...v2.19.2)

Updates `nanoid` from 3.3.6 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.6...3.3.8)

Updates `node-forge` from 1.3.1 to 1.3.3
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.3)

Updates `tar` from 6.1.13 to 6.2.1
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.1.13...v6.2.1)

Updates `next` from 13.4.12 to 14.2.35
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.12...v14.2.35)

Updates `@mozilla/readability` from 0.4.4 to 0.6.0
- [Changelog](https://github.com/mozilla/readability/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mozilla/readability/compare/0.4.4...0.6.0)

Updates `ai` from 4.3.19 to 5.0.113
- [Release notes](https://github.com/vercel/ai/releases)
- [Changelog](https://github.com/vercel/ai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vercel/ai/compare/ai@4.3.9...ai@5.0.52)

Updates `nodemailer` from 6.10.1 to 7.0.11
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.3...v7.0.11)

Updates `next` from 13.4.12 to 14.2.35
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.12...v14.2.35)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 3.2.4
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: formidable
  dependency-version: 3.5.4
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.35
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next-auth
  dependency-version: 4.24.12
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: playwright
  dependency-version: 1.55.1
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.3
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@mozilla/readability"
  dependency-version: 0.6.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: ai
  dependency-version: 5.0.52
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: nodemailer
  dependency-version: 7.0.11
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@babel/runtime"
  dependency-version: 7.27.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: braces
  dependency-version: 3.0.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: follow-redirects
  dependency-version: 1.15.11
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: form-data
  dependency-version: 3.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: jose
  dependency-version: 4.15.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: micromatch
  dependency-version: 4.0.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: min-document
  dependency-version: 2.19.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-version: 3.3.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: node-forge
  dependency-version: 1.3.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: tar
  dependency-version: 6.2.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.35
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@mozilla/readability"
  dependency-version: 0.6.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: ai
  dependency-version: 5.0.113
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: nodemailer
  dependency-version: 7.0.11
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.35
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-13 20:47:58 +00:00
daniel31x13
03ca0c5e3d fix(web): update version to v2.13.2 in package.json 2025-12-12 10:55:24 -05:00
Daniel
1ee3e01cfd Merge pull request #1514 from linkwarden/dev
New Crowdin updates (#1471)
2025-12-11 01:25:58 -05:00
Daniel
e2e9395f8e New Crowdin updates (#1471)
* New translations common.json (Russian)

* New translations common.json (Turkish)

* New translations common.json (Turkish)

* New translations common.json (French)

* New translations common.json (Italian)

* New translations common.json (Russian)

* New translations common.json (Italian)

* New translations common.json (Italian)

---------

Co-authored-by: LinkwardenBot <bot@linkwarden.app>
2025-12-11 01:24:08 -05:00
Daniel
f415e8c4bb Merge pull request #1510 from linkwarden/dev
Dev
2025-12-07 15:30:41 -05:00
daniel31x13
6a73d7c594 minor change 2025-12-07 11:46:45 -05:00
daniel31x13
24206c0953 fix(mobile): integrate react-native-edge-to-edge for improved UI handling and add SafeAreaView for better layout management 2025-12-06 22:14:33 -05:00
daniel31x13
7610f844f7 minor improvement 2025-11-23 13:18:15 -05:00
daniel31x13
a7c1aeb876 fix(mobile): fix image not loading on iOS 2025-11-23 12:42:43 -05:00
daniel31x13
dd4c925a56 remove unnecessary plugin 2025-11-22 20:30:55 -05:00
daniel31x13
a4c55fb455 feat(mobile): add offline caching for all formats + bug fix 2025-11-22 20:28:04 -05:00
daniel31x13
451d17a2cb minor change 2025-11-21 05:48:56 -05:00
daniel31x13
530a12a86f mobile: bug fix + code cleanup 2025-11-21 05:33:53 -05:00
daniel31x13
e908f9c534 feat(mobile+web): add open in default browser option + cleaner code 2025-11-20 06:59:13 -05:00
daniel31x13
9af731c7eb feat(mobile): add open in browser option to context menu 2025-11-17 17:49:02 -05:00
daniel31x13
0548331937 mobile: fix android icon 2025-11-16 18:20:11 -05:00
daniel31x13
9eb4c883ff mobile: bug fix 2025-11-16 17:07:58 -05:00
daniel31x13
17b578361a mobile: minor fix 2025-11-16 16:58:57 -05:00
daniel31x13
08a220f424 small fix 2025-11-16 15:54:53 -05:00
daniel31x13
d7b6ce04e8 mobile: bug fix and DRYer code 2025-11-16 15:49:12 -05:00
daniel31x13
ece88eed5c feat(mobile): refetch loading previews 2025-11-16 15:29:50 -05:00
daniel31x13
257bdf9877 feat(mobile): add loader to preview 2025-11-16 14:44:20 -05:00
daniel31x13
0c512345b1 revert clickable dashboard stats 2025-11-16 13:23:08 -05:00
daniel31x13
9fc9e597e1 fix(mobile): replace router.push with router.navigate to prevent duplicate screens 2025-11-16 10:29:15 -05:00
daniel31x13
03ffc3c379 feat(mobile): clickable dashboard stats 2025-11-16 10:17:15 -05:00
daniel31x13
eb66d72589 feat(mobile): add tags page + many improvements 2025-11-16 10:10:16 -05:00
daniel31x13
3ab026aa37 feat(mobile): add collection page 2025-11-16 07:16:15 -05:00
daniel31x13
bebbf5ad80 feat(mobile): add collection functionality 2025-11-16 06:03:43 -05:00
daniel31x13
8f1612d10b feat(mobile): add share, edit, pin, delete and open in web browser support inside opened link pages 2025-11-15 15:58:35 -05:00
daniel31x13
26da55dfb9 feat(mobile): add loading indicator to link listing pages 2025-11-13 05:07:18 -05:00
daniel31x13
ffee9d0551 feat(mobile): add loading indicator to buttons 2025-11-13 03:19:49 -05:00
daniel31x13
fa4d9313ff fix(mobile): fix pull to refresh disappearing on iOS 2025-11-12 14:42:38 -05:00
daniel31x13
0ff5092561 fix(mobile): fix iPhone 17 crashing bug 2025-11-12 10:51:49 -05:00
daniel31x13
fd676eda34 fix(mobile): set theme as system defaults 2025-11-08 12:37:45 -05:00
daniel31x13
c46583131e fix(mobile): fix collection and tags not updating bug 2025-11-08 12:35:26 -05:00
daniel31x13
1d8fadf18d append .env.local to gitignore 2025-11-08 11:51:34 -05:00
daniel31x13
6b988193b3 fix: adjust SVG height calculation for responsive design in HomeScreen 2025-11-01 11:28:31 -04:00
Daniel
aab7c703ad Merge pull request #1477 from linkwarden/dev
feat: add error handling for dashboard layout updates with toast noti…
2025-10-30 18:34:00 +03:30
daniel31x13
98c1a1f035 feat: add error handling for dashboard layout updates with toast notifications 2025-10-30 11:03:17 -04:00
Daniel
f51f5f376f Merge pull request #1476 from linkwarden/dev
Dev
2025-10-30 18:16:47 +03:30
daniel31x13
e0f3bc3bc2 feat: add service account file configuration and update dashboard API for demo mode 2025-10-30 10:44:04 -04:00
Daniel
c839e7ac4c New Crowdin updates (#1460)
* New translations common.json (French)

* New translations common.json (French)

---------

Co-authored-by: LinkwardenBot <bot@linkwarden.app>
2025-10-22 18:04:28 -04:00
Daniel
b620bbaad7 Merge pull request #1466 from linkwarden/dev
Dev
2025-10-22 16:07:17 +03:30
daniel31x13
21779df1c2 fix: email feedback during trial 2025-10-22 08:36:24 -04:00
daniel31x13
86e721c159 update trial period messaging and conditions in Subscribe component 2025-10-20 13:54:24 -04:00
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
Daniel
9fb24a7329 Merge pull request #1417 from linkwarden/dev
v2.13.0
2025-09-25 09:39:51 -04:00
Daniel
3023316087 Merge pull request #1401 from linkwarden/i18n
New Crowdin updates
2025-09-25 09:38:08 -04:00
Daniel
b5e8b00125 Merge pull request #1416 from linkwarden/feat/improvements
Feat/improvements
2025-09-25 09:37:21 -04:00
daniel31x13
3c3d2d94fa updated labels 2025-09-25 09:25:00 -04:00
daniel31x13
3476d5fbf0 bug fixed 2025-09-24 19:57:01 -04:00
daniel31x13
4ca5c5a177 minor fix 2025-09-24 19:34:21 -04:00
daniel31x13
feedf88b97 minor fix 2025-09-24 19:16:05 -04:00
daniel31x13
80b8029f50 bug fixed 2025-09-24 17:44:40 -04:00
daniel31x13
fc328f3fd3 minor fix 2025-09-24 16:34:58 -04:00
daniel31x13
6a40a70e6d small improvement 2025-09-24 15:32:36 -04:00
daniel31x13
b336b04d71 remove banner 2025-09-24 13:11:21 -04:00
daniel31x13
d1e3badf21 add banner 2025-09-22 10:32:42 -04:00
daniel31x13
3b916dfb71 bug fixed 2025-09-22 09:43:40 -04:00
daniel31x13
6f0ee8eb73 improvements 2025-09-22 09:36:24 -04:00
daniel31x13
20b538c1fb minor improvement 2025-09-22 08:58:51 -04:00
daniel31x13
4541f1435b minor fix 2025-09-22 08:57:49 -04:00
daniel31x13
37c1100e37 improvements 2025-09-22 08:55:49 -04:00
LinkwardenBot
3cc1be0a9d New translations common.json (Dutch) 2025-09-21 16:04:28 +00:00
LinkwardenBot
4b2c2ad66d New translations common.json (Chinese Simplified) 2025-09-21 16:04:28 +00:00
LinkwardenBot
28b755fe37 New translations common.json (Romanian) 2025-09-21 16:04:28 +00:00
LinkwardenBot
c021753585 New translations common.json (English) 2025-09-21 16:04:28 +00:00
LinkwardenBot
bcb9d671c4 New translations common.json (Chinese Traditional) 2025-09-21 16:04:28 +00:00
LinkwardenBot
d2d9477493 New translations common.json (Ukrainian) 2025-09-21 16:04:28 +00:00
LinkwardenBot
4951f8113d New translations common.json (Polish) 2025-09-21 16:04:28 +00:00
LinkwardenBot
67a2c62d26 New translations common.json (Dutch) 2025-09-21 16:04:28 +00:00
LinkwardenBot
8a0264081b New translations common.json (Japanese) 2025-09-21 16:04:28 +00:00
LinkwardenBot
c30ed6c784 New translations common.json (Italian) 2025-09-21 16:04:28 +00:00
LinkwardenBot
ab0a91cf28 New translations common.json (German) 2025-09-21 16:04:28 +00:00
LinkwardenBot
9b5c7909ec New translations common.json (Spanish) 2025-09-21 16:04:28 +00:00
LinkwardenBot
3aa9e33b4e New translations common.json (French) 2025-09-21 16:04:28 +00:00
LinkwardenBot
7e387e504d New translations common.json (Russian) 2025-09-21 16:04:28 +00:00
LinkwardenBot
d689b24d07 New translations common.json (Portuguese, Brazilian) 2025-09-21 16:04:28 +00:00
LinkwardenBot
84de737d92 New translations common.json (Chinese Simplified) 2025-09-21 16:04:28 +00:00
LinkwardenBot
5f6934e7ea New translations common.json (Turkish) 2025-09-21 16:04:28 +00:00
LinkwardenBot
884e6dcf96 New translations common.json (Italian) 2025-09-21 16:04:28 +00:00
LinkwardenBot
2f3e388d1e New translations common.json (Italian) 2025-09-21 16:04:28 +00:00
daniel31x13
bbbd7a242d minor fix 2025-09-19 17:11:48 -04:00
daniel31x13
d91089ed48 bug fixed 2025-09-19 17:09:08 -04:00
daniel31x13
c6bdb8ee5a minor fix 2025-09-19 01:32:38 -04:00
daniel31x13
d9d2e3b78f init 2025-09-18 20:23:40 -04:00
Daniel
7f8d6dcd50 Merge pull request #1406 from linkwarden/feat/tag-management
Feat/tag management
2025-09-17 19:48:34 -04:00
daniel31x13
f4d3b8f657 many fixes and improvements 2025-09-17 19:47:57 -04:00
Daniel
0d1ed7bdd9 Merge pull request #1400 from linkwarden/i18n
New Crowdin updates
2025-09-13 12:50:34 -04:00
LinkwardenBot
01fc6db008 New translations common.json (Italian) 2025-09-13 16:49:59 +00:00
Daniel
1ad0bf14a6 Merge pull request #1373 from linkwarden/i18n
New Crowdin updates
2025-09-13 12:49:11 -04:00
LinkwardenBot
af623cd8e5 New translations common.json (German) 2025-09-10 20:59:34 +00:00
LinkwardenBot
f7a909f18e New translations common.json (German) 2025-09-10 20:59:34 +00:00
LinkwardenBot
3c7a6b53a5 New translations common.json (Dutch) 2025-09-10 20:59:34 +00:00
LinkwardenBot
8b192bedf4 New translations common.json (Romanian) 2025-09-10 20:59:34 +00:00
LinkwardenBot
a57130f838 New translations common.json (Romanian) 2025-09-10 20:59:34 +00:00
LinkwardenBot
6ffed2568a New translations common.json (Romanian) 2025-09-10 20:59:34 +00:00
LinkwardenBot
f7a796fc2b New translations common.json (Romanian) 2025-09-10 20:59:34 +00:00
LinkwardenBot
e55bf7064e New translations common.json (Romanian) 2025-09-10 20:59:34 +00:00
LinkwardenBot
705ec3726b New translations common.json (Japanese) 2025-09-10 20:59:34 +00:00
LinkwardenBot
e343a1829b New translations common.json (Japanese) 2025-09-10 20:59:34 +00:00
LinkwardenBot
cf27bccb6e New translations common.json (German) 2025-09-10 20:59:34 +00:00
LinkwardenBot
07d93c609d New translations common.json (French) 2025-09-10 20:59:33 +00:00
LinkwardenBot
e6818dcbd0 New translations common.json (Italian) 2025-09-10 20:59:33 +00:00
LinkwardenBot
98902a90ac New translations common.json (Polish) 2025-09-10 20:59:33 +00:00
LinkwardenBot
27618029d8 New translations common.json (Spanish) 2025-09-10 20:59:33 +00:00
daniel31x13
9e6a22320b minor fix 2025-09-08 18:01:18 -04:00
daniel31x13
15ce6810c8 feat(mobile): apply dark mode to spinner 2025-09-03 16:19:43 -04:00
daniel31x13
dc237b36f3 fixes and improvements 2025-09-03 15:56:48 -04:00
daniel31x13
4348221210 minor improvement 2025-09-03 13:58:33 -04:00
daniel31x13
cb1f42e0a2 bug fix 2025-09-01 20:17:11 -04:00
Daniel
8526442782 Merge pull request #1382 from khanguyen74/fix-no-title-single-file-integration
Fix link title not being set when saving via SingleFile extension
2025-08-29 20:04:28 -04:00
daniel31x13
a75ac115de minor change 2025-08-29 16:26:54 -04:00
daniel31x13
06e04fa11e improvements 2025-08-29 15:30:59 -04:00
daniel31x13
3dfa53cf01 small improvement 2025-08-29 11:40:24 -04:00
Kha Nguyen
4acc639a8a fix: add link title when creating via SingleFile extension 2025-08-28 22:43:57 -05:00
daniel31x13
07665cee7e feat(mobile): improvements 2025-08-28 20:53:57 -04:00
Daniel
9aefa3cf3b Merge pull request #1379 from linkwarden/dev
bug fix
2025-08-28 16:33:53 -04:00
daniel31x13
36be3d8772 bug fix 2025-08-28 16:33:07 -04:00
Daniel
1af9aaf11f Merge pull request #1372 from linkwarden/dev
Dev
2025-08-27 13:41:28 -04:00
daniel31x13
69b86a473a Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-08-27 13:40:58 -04:00
daniel31x13
22fde2d367 bump version 2025-08-27 13:40:57 -04:00
Daniel
2b63d7e863 Merge pull request #1371 from linkwarden/dev
Dev
2025-08-27 13:38:20 -04:00
Daniel
0aa6b0b4ae Merge pull request #1369 from linkwarden/i18n
New Crowdin updates
2025-08-27 13:37:46 -04:00
daniel31x13
ee489534ec bump version 2025-08-27 13:35:16 -04:00
LinkwardenBot
9f9d96edfe New translations common.json (Russian) 2025-08-27 16:16:00 +00:00
Daniel
cf71bb8a8a Merge pull request #1367 from linkwarden/dev
v2.12.0
2025-08-27 11:19:44 -04:00
daniel31x13
9ed374c4c3 minor fix 2025-08-27 11:02:26 -04:00
daniel31x13
1119f80ffc fix(web): bug fixed 2025-08-27 08:48:33 -04:00
daniel31x13
0f62beb0ab minor improvement 2025-08-26 21:44:53 -04:00
daniel31x13
395f5c01e1 init 2025-08-26 17:14:20 -04:00
daniel31x13
803f344680 minor improvement 2025-08-26 12:05:56 -04:00
daniel31x13
5f243d4fa2 mobile: improved login page 2025-08-25 18:53:56 -04:00
daniel31x13
dcd6b1be48 feat(mobile): clear cache when unauthenticated 2025-08-25 13:18:58 -04:00
daniel31x13
fed53fd187 fix(mobile): bug fix and improvements 2025-08-22 17:51:52 -04:00
daniel31x13
e30bcaefe3 feat(mobile): option to choose links default behavior 2025-08-22 15:50:24 -04:00
daniel31x13
a81111dfe5 feat(mobile): persist theme switching 2025-08-22 15:12:02 -04:00
daniel31x13
ce99562e16 fix(web): lower the minimum height for link cards 2025-08-22 13:46:19 -04:00
daniel31x13
c849e0aeda minor fix 2025-08-22 12:35:56 -04:00
daniel31x13
6f87e02ebc feat(mobile): share intent functionality fully implemented + bug fix 2025-08-22 11:57:51 -04:00
daniel31x13
59ef7baeeb fix(mobile): android dark mode bug 2025-08-22 10:58:58 -04:00
daniel31x13
47068d3352 fix(mobile): minor improvement 2025-08-22 10:19:05 -04:00
daniel31x13
9153958811 feat(mobile): add share sheet page 2025-08-21 14:36:19 -04:00
Daniel
3adbb10be8 Merge pull request #1357 from linkwarden/v2.11.8
V2.11.8
2025-08-20 16:26:27 -04:00
Daniel
5f3bfa4bda Merge pull request #1356 from linkwarden/v2.11.8
V2.11.8
2025-08-20 14:46:47 -04:00
Daniel
e5a278bb6a Merge branch 'main' into v2.11.8 2025-08-20 14:46:06 -04:00
Daniel
4bc3eb4bb5 Merge pull request #1350 from linkwarden/i18n
New Crowdin updates
2025-08-20 14:44:01 -04:00
Daniel
20fbc1552d Merge pull request #1353 from linkwarden/feat/update-stripes-api
Update Stripe's API
2025-08-20 14:40:12 -04:00
Daniel
7cf3812e2c Merge branch 'v2.11.8' into feat/update-stripes-api 2025-08-20 14:40:04 -04:00
Daniel
1ce72e6a03 Merge pull request #1355 from linkwarden/fix/link-batch-selection
Fix/link batch selection
2025-08-20 14:37:08 -04:00
daniel31x13
711477743f bump version 2025-08-20 14:36:40 -04:00
daniel31x13
1c55ac571a remove extra log 2025-08-20 13:59:10 -04:00
daniel31x13
5128efc05c bug fix 2025-08-20 13:43:24 -04:00
daniel31x13
2b6f48ad83 fix bug 2025-08-20 11:59:28 -04:00
LinkwardenBot
435a44dd47 New translations common.json (Portuguese, Brazilian) 2025-08-19 06:14:51 +00:00
LinkwardenBot
244f78a686 New translations common.json (Russian) 2025-08-19 06:14:50 +00:00
daniel31x13
fbb9f7af23 minor fix 2025-08-17 08:32:27 -04:00
daniel31x13
c08da0f990 use new api 2025-08-17 07:18:28 -04:00
Daniel
495d3111a1 Merge pull request #1349 from linkwarden/hotfix
minor fix
2025-08-14 13:11:21 -04:00
Daniel
c8f63cedc1 Merge pull request #1348 from linkwarden/hotfix
minor fix
2025-08-14 13:10:54 -04:00
daniel31x13
221930f88d minor fix 2025-08-14 13:10:24 -04:00
Daniel
58812811d0 Merge pull request #1346 from linkwarden/hotfix
Hotfix
2025-08-14 12:54:02 -04:00
Daniel
f2b521d34b Merge pull request #1345 from linkwarden/hotfix
small fix for new self-hosted users
2025-08-14 12:53:32 -04:00
daniel31x13
56b6791621 bump version 2025-08-14 12:52:42 -04:00
daniel31x13
bd26f90738 small fix for new self-hosted users 2025-08-14 12:51:55 -04:00
daniel31x13
63782f1627 fix(mobile): minor fix to the dashboard page 2025-08-13 18:26:44 -04:00
daniel31x13
078e38c7d8 remove unused imports 2025-08-13 15:21:31 -04:00
Daniel
11806563c8 Merge pull request #1343 from linkwarden/fix/fairer-link-picking
Fix/fairer link picking
2025-08-13 01:06:42 -04:00
Daniel
6da64e6535 Merge pull request #1342 from linkwarden/fix/fairer-link-picking
use a single browser with separate context
2025-08-13 00:11:47 -04:00
daniel31x13
577f8600ae use a single browser with separate context 2025-08-13 00:10:18 -04:00
Daniel
e6c2bc860f Merge pull request #1341 from linkwarden/fix/fairer-link-picking
remove unused code
2025-08-12 22:54:23 -04:00
daniel31x13
c54fbbc985 remove unused code 2025-08-12 22:54:01 -04:00
Daniel
c7d38733ce Merge pull request #1340 from linkwarden/fix/fairer-link-picking
improvement
2025-08-12 22:53:24 -04:00
daniel31x13
794c2f2657 improvement 2025-08-12 22:52:33 -04:00
Daniel
d73f236b36 Merge pull request #1339 from linkwarden/fix/fairer-link-picking
improvement
2025-08-12 22:09:09 -04:00
daniel31x13
658434afaf improvement 2025-08-12 22:08:38 -04:00
Daniel
a191861748 Merge pull request #1338 from linkwarden/fix/fairer-link-picking
fix user blocking queue
2025-08-12 18:21:27 -04:00
daniel31x13
eb3f1eeb5b fix user blocking queue 2025-08-12 17:58:25 -04:00
daniel31x13
c911f132f6 add native folders to gitignore 2025-08-12 11:05:48 -04:00
daniel31x13
89122ccd5c feat(mobile): add share sheet functionality 2025-08-12 10:39:01 -04:00
daniel31x13
ac8dacd570 fix(mobile): refetch on mount 2025-08-11 16:02:14 -04:00
daniel31x13
0b942cbb29 feat(web): add singlefile upload route 2025-08-11 15:11:32 -04:00
Daniel
f8cfe8e556 Merge pull request #1332 from khanguyen74/feat/allow-upload-html-archive
Feat: allow uploading archives
2025-08-11 12:00:13 -04:00
daniel31x13
2be7f314bb made the upload functionality api-only 2025-08-11 11:59:56 -04:00
Kha Nguyen
f083b45b78 allow uploading asset if user has access to collection 2025-08-10 22:51:55 -05:00
Kha Nguyen
0e7d2ef716 add Crowdin for failed upload message 2025-08-10 21:49:36 -05:00
Kha Nguyen
581e27e8c6 feat(web): add upload button to preservation nav bar 2025-08-10 21:42:11 -05:00
Kha Nguyen
c20f6d8015 remove upload buttons on link detail drawer 2025-08-10 21:40:45 -05:00
daniel31x13
91dafd40f1 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-08-10 18:12:58 -04:00
daniel31x13
d0cf951731 minor fix 2025-08-10 18:12:57 -04:00
Daniel
c75685c435 Merge pull request #1330 from khanguyen74/fix/collection-list-sort
Fix: properly handle collection list on dashboard page
2025-08-10 17:31:36 -04:00
Kha Nguyen
3bc8bb676e fix: properly sort collection list on dashboard page 2025-08-10 12:08:59 -05:00
Daniel
63f77b19a9 Merge pull request #1329 from linkwarden/i18n
New Crowdin updates
2025-08-10 07:38:24 -04:00
Daniel
8521f4f1e9 Merge pull request #1325 from teynar/fix/mismatched-link-favicon-size
Fix: request 64px gstatic favicon for consistent icon display
2025-08-10 07:34:50 -04:00
Daniel
a866de6931 Merge pull request #1331 from khanguyen74/fix/update-drag-n-drop-search-page
fix: properly handle drag-and-drop
2025-08-10 07:27:26 -04:00
daniel31x13
d661c5c609 minor fix 2025-08-10 07:15:09 -04:00
daniel31x13
7b12a82a67 Revert "Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev" 2025-08-08 17:43:36 -04:00
daniel31x13
374c64ff0f Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-08-08 17:35:34 -04:00
daniel31x13
7558068f7a minor update 2025-08-08 17:35:29 -04:00
Kha Nguyen
c03557c080 ask for confirmation before refresh preserved data 2025-08-08 14:34:29 -05:00
daniel31x13
f15b8df6f3 feat(mobile): share sheet base commit 2025-08-08 14:14:37 -04:00
Kha Nguyen
91e337ea8c add options to replace other preserved formats 2025-08-08 13:10:44 -05:00
Kha Nguyen
65f0075a6f feat(web): add option to upload html archive 2025-08-08 12:37:29 -05:00
Kha Nguyen
a8928e9de4 fix: change drag handle to inside link card
- fixes issue where dragging link details drawer also causing the link
to be dragged
2025-08-08 10:21:32 -05:00
Kha Nguyen
fb7e2286a7 fix(web): use DragNDrop wrapper for search page 2025-08-08 09:02:25 -05:00
LinkwardenBot
f057434c3b New translations common.json (Chinese Simplified) 2025-08-08 04:09:14 +00:00
LinkwardenBot
c7c4959659 New translations common.json (Chinese Simplified) 2025-08-08 04:09:14 +00:00
LinkwardenBot
c6ed7bb8e8 New translations common.json (Chinese Traditional) 2025-08-08 04:09:14 +00:00
LinkwardenBot
7351b5265d New translations common.json (Ukrainian) 2025-08-08 04:09:14 +00:00
LinkwardenBot
fcc4b89249 New translations common.json (Polish) 2025-08-08 04:09:14 +00:00
LinkwardenBot
04ea06e319 New translations common.json (Dutch) 2025-08-08 04:09:14 +00:00
LinkwardenBot
57347f2b90 New translations common.json (Japanese) 2025-08-08 04:09:14 +00:00
LinkwardenBot
35ae395447 New translations common.json (Italian) 2025-08-08 04:09:14 +00:00
LinkwardenBot
553680d9eb New translations common.json (German) 2025-08-08 04:09:14 +00:00
LinkwardenBot
1048989598 New translations common.json (Spanish) 2025-08-08 04:09:14 +00:00
LinkwardenBot
b5f96f5ed3 New translations common.json (French) 2025-08-08 04:09:14 +00:00
LinkwardenBot
e22ed9bb3d New translations common.json (Russian) 2025-08-08 04:09:13 +00:00
LinkwardenBot
51ee97c7ef New translations common.json (Portuguese, Brazilian) 2025-08-08 04:09:13 +00:00
LinkwardenBot
c25bc658f4 New translations common.json (Chinese Simplified) 2025-08-08 04:09:13 +00:00
LinkwardenBot
3b8a6051e9 New translations common.json (Turkish) 2025-08-08 04:09:13 +00:00
Kha Nguyen
b8a6c77150 fix(web): fix collection sortable list on dashboard page 2025-08-07 20:41:17 -05:00
daniel31x13
a9937d218c minor update 2025-08-07 20:10:43 -04:00
daniel31x13
d3302daa3a remove comment 2025-08-07 18:34:17 -04:00
Daniel
dac2b4c30a Merge pull request #1322 from khanguyen74/fix/email-input-disappear
Fix: email input disappear in user modal
2025-08-07 18:26:17 -04:00
Daniel
28e3e23f8a Merge pull request #1310 from khanguyen74/fix/subscription-check-fix
Fix subscription check blocking user creation in admin dashboard
2025-08-07 18:04:29 -04:00
Daniel
80375843af Merge pull request #1320 from khanguyen74/feature/add-drag-and-drop
feat: add drag and drop to move links between collection on dashboard page
2025-08-07 17:41:46 -04:00
daniel31x13
232098191a a bit more concise alert 2025-08-07 17:33:39 -04:00
Kha Nguyen
b59ad36192 feat(web): add drag and drop to tag on sidebar 2025-08-07 15:13:01 -05:00
Kha Nguyen
ebdc8b9db2 refactor drag and drop pages 2025-08-07 15:12:36 -05:00
Kha Nguyen
7a76de5726 add TagListing component
refactor tags out of sidebar
2025-08-07 13:49:28 -05:00
Kha Nguyen
a0ceca443c feat(web): add drag-n-drop to tag page 2025-08-07 13:18:54 -05:00
Kha Nguyen
21cb65edbd clean up 2025-08-07 12:55:05 -05:00
Kha Nguyen
c58f8f3086 feat(web): add drag-n-drop to collection page 2025-08-07 12:53:03 -05:00
Kha Nguyen
04419aa9e0 feat(web): add drag-n-drop to search page 2025-08-07 12:47:58 -05:00
Kha Nguyen
e916ea53fc feat(web): add drag-n-drop to pinned page 2025-08-07 12:36:59 -05:00
Kha Nguyen
20fde711d1 refactor links page 2025-08-07 12:36:41 -05:00
Kha Nguyen
652931e5be prevent hover effect on sidebar collection list when dragging over 2025-08-07 12:21:56 -05:00
Daniel
88d4e0203f Merge pull request #1302 from linkwarden/i18n
New Crowdin updates
2025-08-07 12:57:32 -04:00
Kha Nguyen
87ae5da6f3 add drag-n-drop to masonry view 2025-08-07 00:20:33 -05:00
Kha Nguyen
6a9a897529 add drag-n-drop to list view 2025-08-07 00:18:51 -05:00
Kha Nguyen
92a1c4a5f0 disable drag and drop on small screen on links page 2025-08-07 00:14:56 -05:00
Kha Nguyen
1ca2b4e534 refactor how selected link className is applied 2025-08-06 23:58:31 -05:00
daniel31x13
139f99d050 fix(mobile): fix android not updating queries 2025-08-06 20:32:59 -04:00
daniel31x13
c5bfd20833 fix(mobile): bug fixed 2025-08-06 19:55:27 -04:00
Kha Nguyen
26ab9584ca use favicon as overlay when dragging links 2025-08-06 18:38:28 -05:00
Kha Nguyen
76056d6b1a remove drag handle 2025-08-06 18:36:54 -05:00
daniel31x13
bc129af07a fix(mobile): apply dark mode to android statusbar 2025-08-06 19:34:05 -04:00
Kha Nguyen
16a83496bc ui change to droppable area 2025-08-06 18:31:06 -05:00
Kha Nguyen
e270a6b957 use favicon as overlay component when dragging 2025-08-06 17:27:10 -05:00
Kha Nguyen
ae6dbf7745 remove dropping link placeholder 2025-08-06 17:03:40 -05:00
daniel31x13
ee8a6634f0 fix(mobile): bug fix 2025-08-06 17:46:34 -04:00
Teynar
a7f8de0a2e fix(link-icon): request 64px gstatic favicon for consistent icon display
Previously, a 32px favicon was requested and scaled to 64px, causing blurriness.
2025-08-06 23:39:59 +02:00
Kha Nguyen
1f369401df drag/drop ui improvement 2025-08-06 16:24:04 -05:00
Kha Nguyen
a4375e3b57 update links page 2025-08-06 15:23:15 -05:00
Kha Nguyen
532f6fb1f1 make LinkCard draggable 2025-08-06 15:22:56 -05:00
daniel31x13
1e1bd8f333 fix(mobile): bugs fixed 2025-08-06 16:17:28 -04:00
Kha Nguyen
c999259102 refactor custom collision algorithm function 2025-08-06 15:03:03 -05:00
Kha Nguyen
13883e0cbc add drag and drop to links page 2025-08-06 15:02:19 -05:00
Kha Nguyen
5acfdc2f89 update logic to set active link when the link is dragged 2025-08-06 14:10:32 -05:00
Kha Nguyen
8e1767d7d0 fix(web): fix user modal not showing email input
- properly check for EMAIL_PROVIDER environment variable
2025-08-05 22:22:09 -05:00
Kha Nguyen
477f534a17 revert className changes in LinkPin component 2025-08-05 20:46:44 -05:00
daniel31x13
032359a357 fix(mobile): fix paddings on android 2025-08-05 19:18:16 -04:00
daniel31x13
87c66b81e0 feat(mobile): add cross-platform icon library 2025-08-05 18:03:23 -04:00
LinkwardenBot
8eb3b5ce5d New translations common.json (Russian) 2025-08-05 21:48:41 +00:00
LinkwardenBot
ee13d3551d New translations common.json (Turkish) 2025-08-05 21:48:41 +00:00
LinkwardenBot
52a4441b6f New translations common.json (Portuguese, Brazilian) 2025-08-05 21:48:41 +00:00
LinkwardenBot
04e73942a6 New translations common.json (Portuguese, Brazilian) 2025-08-05 21:48:41 +00:00
LinkwardenBot
6234278c9f New translations common.json (Turkish) 2025-08-05 21:48:41 +00:00
LinkwardenBot
e39d0b1bbd New translations common.json (Turkish) 2025-08-05 21:48:41 +00:00
LinkwardenBot
b703eb5654 New translations common.json (Chinese Simplified) 2025-08-05 21:48:40 +00:00
LinkwardenBot
0b296dc557 New translations common.json (Chinese Simplified) 2025-08-05 21:48:40 +00:00
LinkwardenBot
3ba2beb769 New translations common.json (Chinese Simplified) 2025-08-05 21:48:40 +00:00
LinkwardenBot
4b477f82bf New translations common.json (Turkish) 2025-08-05 21:48:40 +00:00
LinkwardenBot
30ef7863d5 New translations common.json (Turkish) 2025-08-05 21:48:40 +00:00
daniel31x13
b78aa39f79 fix(mobile): bugs fixed 2025-08-05 16:42:32 -04:00
Kha Nguyen
787e4c49a4 remove LinkAction open control prop
should be addressed in a separate PR
2025-08-03 09:04:55 -05:00
Kha Nguyen
6e60031e40 update collision algorithm 2025-08-02 23:22:29 -05:00
Kha Nguyen
23148b22d7 feat: the whole dashboard link is now draggable 2025-08-02 23:14:58 -05:00
Kha Nguyen
07945f946d improve mobile support for drag and drop 2025-08-02 23:01:45 -05:00
Kha Nguyen
702508498e allow drag and drop link to collections in side bar 2025-08-02 21:07:26 -05:00
Kha Nguyen
289818716c optimistically update pinned link when drop to pinned links section 2025-08-02 18:49:41 -05:00
Kha Nguyen
75419df40b feat: show link placeholder on pinned link section on drag 2025-08-02 18:16:34 -05:00
Kha Nguyen
586ef5ed58 feat: show placeholder on target collection 2025-08-02 16:56:21 -05:00
daniel31x13
ee53a170ed feat(mobile): add system defaults to themes 2025-07-31 20:13:28 -04:00
daniel31x13
ea48e797e3 small fix 2025-07-31 20:02:29 -04:00
daniel31x13
b294abd65b minor improvement 2025-07-31 18:54:47 -04:00
daniel31x13
f7fcbfe635 minor improvement 2025-07-31 18:48:05 -04:00
daniel31x13
2c01013eb3 feat(mobile): improved theme colors 2025-07-31 17:26:28 -04:00
daniel31x13
a36af7d673 feat(mobile): completed implementing dark mode 2025-07-31 16:21:57 -04:00
daniel31x13
f4c8030a1b feat(mobile): initial commit for theme settings 2025-07-30 19:44:57 -04:00
Daniel
0a735cd2f6 Merge pull request #1313 from linkwarden/dev
Dev
2025-07-29 19:12:59 -04:00
daniel31x13
d870ffecd1 feat(web): optimization 2025-07-29 19:11:02 -04:00
Kha Nguyen
360eadb08d show toast when drop a link to collection it already belongs to
- only applies if dragged from recent section
2025-07-28 19:43:24 -05:00
Kha Nguyen
35eec60ac9 improve drag n drop on touch devices 2025-07-28 19:01:18 -05:00
daniel31x13
dae7d2be3b feat(mobile): small improvement to the settings page 2025-07-28 16:19:49 -04:00
daniel31x13
e0abe1df39 feat(mobile): improved dashboard 2025-07-28 16:01:55 -04:00
Kha Nguyen
4ae03168bb optimistically update the collection after dropping link 2025-07-28 12:38:35 -05:00
Kha Nguyen
9a6429b85b update how link card look while dragging 2025-07-28 11:45:11 -05:00
Kha Nguyen
20997ba8bc feat(web): change link drag and drop logic 2025-07-28 00:55:53 -05:00
Kha Nguyen
4fe2ef7134 feat(web): handle pinning link by drag-n-drop 2025-07-27 01:29:43 -05:00
Kha Nguyen
63b6f3e66a chore: add dependencies 2025-07-26 01:33:06 -05:00
Kha Nguyen
5c07687e1a fix(web): show more button on link card while dropdown menu opens 2025-07-26 01:32:51 -05:00
Kha Nguyen
019bc783a4 feat(web): allow drag and drop to move a link to new collection 2025-07-26 01:31:13 -05:00
Kha Nguyen
3dfa12dbbf feat(web): add droppable dashboard links 2025-07-26 01:30:13 -05:00
Kha Nguyen
20eecd682e fix: only require subscription check when Stripe is enabled 2025-07-25 20:41:58 -05:00
daniel31x13
de3ca46ef0 feat(mobile): finished building the dashboard 2025-07-24 18:32:55 -04:00
daniel31x13
a77c5c20cd feat(mobile): add copy to clipboard functionality 2025-07-23 22:22:33 -04:00
daniel31x13
822415c695 feat(mobile): add edit link functionality 2025-07-22 20:11:46 -04:00
daniel31x13
3de6e9965a chore(mobile): implement sheet manager 2025-07-22 12:19:48 -04:00
daniel31x13
8555d26d99 feat(mobile): add link functionality + delete link functionality 2025-07-21 17:40:27 -04:00
daniel31x13
c8c8fb7875 bug fixed 2025-07-21 13:50:44 -04:00
Daniel
64ae49a3a4 Merge pull request #1299 from linkwarden/dev
bug fixed
2025-07-19 23:13:58 -04:00
daniel31x13
1bcbf2011d bump version 2025-07-19 23:13:41 -04:00
daniel31x13
d43c4e3e95 bug fixed 2025-07-19 23:10:10 -04:00
Daniel
6253635e78 Merge pull request #1296 from linkwarden/dev
minor fix
2025-07-18 16:33:21 -04:00
daniel31x13
b30f554fa7 minor fix 2025-07-18 16:10:06 -04:00
Daniel
f80432eda1 Merge pull request #1292 from linkwarden/dev
Dev
2025-07-16 17:52:28 -04:00
Daniel
267a14c535 Merge pull request #1293 from linkwarden/i18n
New Crowdin updates
2025-07-16 17:51:40 -04:00
daniel31x13
e24e338e67 update readme 2025-07-16 17:44:31 -04:00
Daniel
0299f7bd4b New translations common.json (Portuguese, Brazilian) 2025-07-16 16:39:16 -04:00
Daniel
db4b570577 New translations common.json (Chinese Traditional) 2025-07-16 16:39:15 -04:00
Daniel
8b5b1ae17e New translations common.json (Chinese Simplified) 2025-07-16 16:39:14 -04:00
Daniel
566927f6ff New translations common.json (Ukrainian) 2025-07-16 16:39:13 -04:00
Daniel
82d37e00e2 New translations common.json (Turkish) 2025-07-16 16:39:11 -04:00
Daniel
9be8b540d0 New translations common.json (Russian) 2025-07-16 16:39:10 -04:00
Daniel
6c01456c76 New translations common.json (Polish) 2025-07-16 16:39:09 -04:00
Daniel
dd6cde5b93 New translations common.json (Dutch) 2025-07-16 16:39:08 -04:00
Daniel
a984f8eca2 New translations common.json (Japanese) 2025-07-16 16:39:07 -04:00
Daniel
9fe910e205 New translations common.json (Italian) 2025-07-16 16:39:06 -04:00
Daniel
c76ac2f4c7 New translations common.json (German) 2025-07-16 16:39:05 -04:00
Daniel
71b1ff7da0 New translations common.json (Spanish) 2025-07-16 16:39:04 -04:00
Daniel
2bb1393eca New translations common.json (French) 2025-07-16 16:39:03 -04:00
daniel31x13
44f094f473 added a confirmation modal for re-preserving links 2025-07-16 16:31:27 -04:00
daniel31x13
ef98de61ee bug fixed 2025-07-14 17:28:10 -04:00
daniel31x13
1897e9c8ce minor fix 2025-07-14 17:07:00 -04:00
daniel31x13
659910a76d minor improvement 2025-07-14 17:02:56 -04: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
Daniel
f3e31edb7d Merge pull request #1289 from linkwarden/dev
v2.11.4
2025-07-12 16:41:22 -04:00
daniel31x13
936f7d9614 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-07-12 15:18:47 -04:00
daniel31x13
966c271bd3 bump version 2025-07-12 15:18:45 -04:00
Daniel
95c243df18 Merge pull request #1285 from linkwarden/i18n
New Crowdin updates
2025-07-12 15:17:23 -04:00
daniel31x13
89efd237fe improved UX 2025-07-12 15:15:56 -04:00
LinkwardenBot
899426772b New translations common.json (Russian) 2025-07-06 12:36:47 +00:00
LinkwardenBot
55582433c6 New translations common.json (Russian) 2025-07-06 12:36:46 +00:00
Daniel
395f357fcb Merge pull request #1282 from linkwarden/dev
v2.11.3
2025-07-05 00:32:01 -04:00
Daniel
a14485c6dd Merge pull request #1272 from linkwarden/i18n
New Crowdin updates
2025-07-05 00:30:26 -04:00
LinkwardenBot
2351a83c48 New translations common.json (Spanish) 2025-07-05 04:29:26 +00:00
LinkwardenBot
e761c1d17a New translations common.json (French) 2025-07-05 04:29:26 +00:00
LinkwardenBot
ea01ab7b0f New translations common.json (Spanish) 2025-07-05 04:29:26 +00:00
LinkwardenBot
583bc077fb New translations common.json (Spanish) 2025-07-05 04:29:26 +00:00
daniel31x13
63ed780bb0 small fix 2025-07-05 00:24:35 -04:00
Tchoupinax
2441470849 fix: add support for password manager for login page 2025-06-29 11:20:18 +02:00
daniel31x13
5032bc3d13 remove extra logging 2025-06-28 14:44:53 -04:00
Daniel
7cec177ef0 Merge pull request #1267 from linkwarden/dev
Dev
2025-06-28 21:01:07 +03:30
daniel31x13
cb104a3a64 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-06-28 13:30:52 -04:00
daniel31x13
3fe98f9346 bump version 2025-06-28 13:30:50 -04:00
Daniel
22a311297a Merge pull request #1266 from linkwarden/i18n
New Crowdin updates
2025-06-28 20:42:41 +03:30
LinkwardenBot
c9da55034c New translations common.json (French) 2025-06-28 17:11:53 +00:00
LinkwardenBot
6d7f69929f New translations common.json (French) 2025-06-28 17:11:53 +00:00
LinkwardenBot
77dd17f051 New translations common.json (German) 2025-06-28 17:11:53 +00:00
daniel31x13
3e015fa76c small fix 2025-06-28 13:10:53 -04:00
Daniel
c56259cb72 Merge pull request #1263 from linkwarden/dev
small fix
2025-06-28 03:57:50 +03:30
daniel31x13
a05fbd7141 small fix 2025-06-27 20:27:03 -04:00
Daniel
e00852f5df Merge pull request #1262 from linkwarden/dev
minor fix for the demo instance
2025-06-28 03:17:27 +03:30
daniel31x13
3067b6e2d3 minor fix for the demo instance 2025-06-27 19:44:00 -04:00
Daniel
a49a57c6ea Merge pull request #1260 from linkwarden/dev
bump version
2025-06-28 00:50:34 +03:30
daniel31x13
32efa56f77 bump version 2025-06-27 17:20:12 -04:00
Daniel
a8c6ef6fa0 Merge pull request #1259 from linkwarden/dev
bug fixed
2025-06-28 00:44:01 +03:30
daniel31x13
21fadfe389 bug fixed 2025-06-27 17:13:23 -04:00
Daniel
1e80fb33c4 Merge pull request #1258 from linkwarden/dev
Dev
2025-06-28 00:37:43 +03:30
daniel31x13
edf72aa042 bug fixed 2025-06-27 17:00:38 -04:00
daniel31x13
74d5dfb404 bug fix 2025-06-27 16:52:23 -04:00
daniel31x13
86971dcead bug fix 2025-06-27 16:25:08 -04:00
daniel31x13
9b0c3bf405 bug fix 2025-06-27 16:07:34 -04:00
Daniel
3ce30ec0f6 Merge pull request #1255 from linkwarden/dev
Dev
2025-06-27 20:34:28 +03:30
daniel31x13
e08684328a improved performance 2025-06-27 13:03:21 -04:00
Daniel
fe06ecc3f6 Merge pull request #1254 from linkwarden/main
sync
2025-06-27 19:24:12 +03:30
daniel31x13
42d877e2b5 small fix 2025-06-27 11:52:54 -04:00
Daniel
94cba5394f Merge pull request #1253 from linkwarden/dev
minor fix
2025-06-27 16:56:46 +03:30
daniel31x13
70999b83c5 minor fix 2025-06-27 09:26:15 -04:00
Daniel
d89aa4a4e7 Merge pull request #1249 from linkwarden/dev
v2.11.0
2025-06-27 16:47:00 +03:30
daniel31x13
c933970cce bug fixed 2025-06-27 08:16:01 -04:00
daniel31x13
8f10930127 add autoFocus to modals 2025-06-27 06:41:23 -04:00
daniel31x13
30c1a064e0 add omit field to link textContent 2025-06-27 06:21:47 -04:00
daniel31x13
d207c89d8e small improvements 2025-06-26 19:39:12 -04:00
daniel31x13
4712dc9b26 bug fixed 2025-06-26 17:55:19 -04:00
daniel31x13
1203f62ab3 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-06-26 16:10:11 -04:00
daniel31x13
c1df0db729 improvements 2025-06-26 16:10:09 -04:00
Daniel
ee5814a0e4 Merge pull request #1248 from linkwarden/i18n
New Crowdin updates
2025-06-26 21:50:46 +03:30
daniel31x13
126d98d1b8 small improvement 2025-06-26 13:54:50 -04:00
daniel31x13
ebbe68dc9c bug fixed 2025-06-26 13:45:08 -04:00
daniel31x13
5a763bdce0 minor fix 2025-06-26 13:37:48 -04:00
Daniel
3b36cf09f4 New translations common.json (Russian) 2025-06-26 20:57:10 +03:30
daniel31x13
fb017a7655 small improvement 2025-06-26 13:18:02 -04:00
daniel31x13
b462531da8 small improvement to the seed script 2025-06-26 13:13:50 -04:00
daniel31x13
acbefe4b6e update logo 2025-06-26 12:43:18 -04:00
LinkwardenBot
e5296dd5c9 New translations common.json (Portuguese, Brazilian) 2025-06-26 15:52:19 +00:00
LinkwardenBot
a8295f94b8 New translations common.json (English) 2025-06-26 15:52:19 +00:00
LinkwardenBot
8607c066b8 New translations common.json (Chinese Traditional) 2025-06-26 15:52:19 +00:00
LinkwardenBot
63bc36d5ef New translations common.json (Chinese Simplified) 2025-06-26 15:52:19 +00:00
LinkwardenBot
74a671d165 New translations common.json (Ukrainian) 2025-06-26 15:52:19 +00:00
LinkwardenBot
b95c4b2d97 New translations common.json (Turkish) 2025-06-26 15:52:19 +00:00
LinkwardenBot
9a4f74e4a9 New translations common.json (Russian) 2025-06-26 15:52:19 +00:00
LinkwardenBot
e43fd9359b New translations common.json (Polish) 2025-06-26 15:52:19 +00:00
LinkwardenBot
be011955b5 New translations common.json (Dutch) 2025-06-26 15:52:19 +00:00
LinkwardenBot
5660093f1a New translations common.json (Japanese) 2025-06-26 15:52:19 +00:00
LinkwardenBot
a7b13e27f9 New translations common.json (Italian) 2025-06-26 15:52:19 +00:00
LinkwardenBot
f25edb1302 New translations common.json (German) 2025-06-26 15:52:19 +00:00
LinkwardenBot
592254daba New translations common.json (Spanish) 2025-06-26 15:52:19 +00:00
LinkwardenBot
395a7a58aa New translations common.json (French) 2025-06-26 15:52:19 +00:00
LinkwardenBot
7fcd73b61b New translations common.json (Portuguese, Brazilian) 2025-06-26 15:52:19 +00:00
LinkwardenBot
b091e133a7 New translations common.json (Chinese Traditional) 2025-06-26 15:52:19 +00:00
LinkwardenBot
5c8e339864 New translations common.json (Chinese Simplified) 2025-06-26 15:52:19 +00:00
LinkwardenBot
35808ef03f New translations common.json (Ukrainian) 2025-06-26 15:52:19 +00:00
LinkwardenBot
1fc7f436a1 New translations common.json (Turkish) 2025-06-26 15:52:19 +00:00
LinkwardenBot
2e15d43e4b New translations common.json (Russian) 2025-06-26 15:52:19 +00:00
LinkwardenBot
8b25a54b26 New translations common.json (Polish) 2025-06-26 15:52:19 +00:00
LinkwardenBot
6c4f694f17 New translations common.json (Dutch) 2025-06-26 15:52:19 +00:00
LinkwardenBot
a6370dd24b New translations common.json (Japanese) 2025-06-26 15:52:19 +00:00
LinkwardenBot
873f51379d New translations common.json (Italian) 2025-06-26 15:52:19 +00:00
LinkwardenBot
701824dd18 New translations common.json (German) 2025-06-26 15:52:19 +00:00
LinkwardenBot
e39876790b New translations common.json (Spanish) 2025-06-26 15:52:19 +00:00
LinkwardenBot
db7135abdf New translations common.json (French) 2025-06-26 15:52:19 +00:00
daniel31x13
0b89ab3e35 minor improvement 2025-06-26 11:07:03 -04:00
Daniel
707ec84ee2 Merge pull request #1247 from linkwarden/i18n
New Crowdin updates
2025-06-26 02:04:38 +03:30
LinkwardenBot
ba03f99666 New translations common.json (Portuguese, Brazilian) 2025-06-25 22:34:12 +00:00
LinkwardenBot
6cbf9c7153 New translations common.json (English) 2025-06-25 22:34:12 +00:00
LinkwardenBot
b230c12f61 New translations common.json (Chinese Traditional) 2025-06-25 22:34:12 +00:00
LinkwardenBot
f3cd1845b7 New translations common.json (Chinese Simplified) 2025-06-25 22:34:12 +00:00
LinkwardenBot
580c87880b New translations common.json (Ukrainian) 2025-06-25 22:34:12 +00:00
LinkwardenBot
52a10aba1e New translations common.json (Turkish) 2025-06-25 22:34:12 +00:00
LinkwardenBot
5dcf73570e New translations common.json (Russian) 2025-06-25 22:34:12 +00:00
LinkwardenBot
5cffa0bd4f New translations common.json (Polish) 2025-06-25 22:34:12 +00:00
LinkwardenBot
6a8bb8b6e8 New translations common.json (Dutch) 2025-06-25 22:34:12 +00:00
LinkwardenBot
3e3a368131 New translations common.json (Japanese) 2025-06-25 22:34:12 +00:00
LinkwardenBot
6b7fca78e1 New translations common.json (Italian) 2025-06-25 22:34:12 +00:00
LinkwardenBot
d356fc9fd5 New translations common.json (German) 2025-06-25 22:34:12 +00:00
LinkwardenBot
f198cc5923 New translations common.json (Spanish) 2025-06-25 22:34:12 +00:00
LinkwardenBot
4e50ead03e New translations common.json (French) 2025-06-25 22:34:12 +00:00
LinkwardenBot
c716a986a3 New translations common.json (Portuguese, Brazilian) 2025-06-25 22:34:12 +00:00
LinkwardenBot
fcf9548552 New translations common.json (English) 2025-06-25 22:34:12 +00:00
LinkwardenBot
918d0d46b2 New translations common.json (Chinese Traditional) 2025-06-25 22:34:12 +00:00
LinkwardenBot
a57ec28f3d New translations common.json (Chinese Simplified) 2025-06-25 22:34:12 +00:00
LinkwardenBot
fc4ba6a1fc New translations common.json (Ukrainian) 2025-06-25 22:34:12 +00:00
LinkwardenBot
b05c96360f New translations common.json (Turkish) 2025-06-25 22:34:12 +00:00
LinkwardenBot
37f13af8aa New translations common.json (Russian) 2025-06-25 22:34:12 +00:00
LinkwardenBot
1e24580010 New translations common.json (Polish) 2025-06-25 22:34:12 +00:00
LinkwardenBot
69c713bf50 New translations common.json (Dutch) 2025-06-25 22:34:12 +00:00
LinkwardenBot
e9853bc6f5 New translations common.json (Japanese) 2025-06-25 22:34:12 +00:00
LinkwardenBot
dbb055699a New translations common.json (Italian) 2025-06-25 22:34:12 +00:00
LinkwardenBot
da3d4e2c0a New translations common.json (German) 2025-06-25 22:34:12 +00:00
LinkwardenBot
27d88c2218 New translations common.json (Spanish) 2025-06-25 22:34:12 +00:00
LinkwardenBot
b59ccfffa1 New translations common.json (French) 2025-06-25 22:34:12 +00:00
daniel31x13
4bc0476e38 small fix 2025-06-25 18:30:51 -04:00
daniel31x13
eba81441ea minor fix 2025-06-25 18:24:59 -04:00
daniel31x13
4bd91f0b95 minor fix 2025-06-25 18:19:22 -04:00
daniel31x13
134757648d small fix 2025-06-25 18:09:31 -04:00
daniel31x13
86a5d0f965 small fix 2025-06-25 18:04:10 -04:00
daniel31x13
8a838f382a add locale-action.yml 2025-06-25 17:54:37 -04:00
daniel31x13
dd2361d1cf update readme 2025-06-25 16:10:16 -04:00
daniel31x13
a251e5e526 Revert "New translations common.json (Portuguese, Brazilian)"
This reverts commit f5633089f9.
2025-06-25 16:08:49 -04:00
daniel31x13
1e8a0fd1c9 Revert "add translation readme"
This reverts commit 1339b5d625.

revert
2025-06-25 15:40:12 -04:00
LinkwardenBot
d4cc066c76 Merge pull request #1245 from linkwarden/LinkwardenBot-patch-1
Update .prettierignore
2025-06-25 15:08:56 -04:00
LinkwardenBot
a1f2f3484b Update .prettierignore 2025-06-25 15:08:02 -04:00
daniel31x13
1339b5d625 add translation readme 2025-06-25 12:59:30 -04:00
Daniel
40abb89602 Merge pull request #1243 from linkwarden/dev-i18n
New Crowdin updates
2025-06-25 18:29:48 +03:30
Daniel
0a8d48a07a New translations common.json (English) 2025-06-25 18:19:34 +03:30
daniel31x13
b3530225f4 remove test 2025-06-25 10:48:40 -04:00
daniel31x13
78726a2f04 crowdin test commit 2025-06-25 10:43:10 -04:00
Daniel
c44f044505 Merge pull request #1242 from linkwarden/dev-i18n
New Crowdin updates
2025-06-25 18:11:19 +03:30
Daniel
8508360dc7 New translations common.json (Portuguese, Brazilian) 2025-06-25 18:09:30 +03:30
Daniel
c16b1c621c New translations common.json (English) 2025-06-25 18:09:28 +03:30
Daniel
e098fe1c39 New translations common.json (Chinese Traditional) 2025-06-25 18:09:27 +03:30
Daniel
7f8c856f6c New translations common.json (Chinese Simplified) 2025-06-25 18:09:26 +03:30
Daniel
8b1a88f326 New translations common.json (Ukrainian) 2025-06-25 18:09:25 +03:30
Daniel
9ced58eae8 New translations common.json (Turkish) 2025-06-25 18:09:24 +03:30
Daniel
3b2e5089d4 New translations common.json (Russian) 2025-06-25 18:09:23 +03:30
Daniel
3822475fa4 New translations common.json (Polish) 2025-06-25 18:09:21 +03:30
Daniel
929e23850b New translations common.json (Dutch) 2025-06-25 18:09:20 +03:30
Daniel
3645c6f0ff New translations common.json (Japanese) 2025-06-25 18:09:19 +03:30
Daniel
1ad44a81a3 New translations common.json (Italian) 2025-06-25 18:09:17 +03:30
Daniel
d4a808e808 New translations common.json (German) 2025-06-25 18:09:16 +03:30
Daniel
3953bb000c New translations common.json (Spanish) 2025-06-25 18:09:15 +03:30
Daniel
2be900a3f0 New translations common.json (French) 2025-06-25 18:09:14 +03:30
Daniel
92d335ddbe Merge pull request #1241 from linkwarden/dev-i18n
New Crowdin updates
2025-06-25 18:00:01 +03:30
Daniel
f5633089f9 New translations common.json (Portuguese, Brazilian) 2025-06-25 17:54:56 +03:30
Daniel
1b6f8f1fc8 New translations common.json (English) 2025-06-25 17:54:55 +03:30
Daniel
5cd0fbdf6f New translations common.json (Chinese Traditional) 2025-06-25 17:54:54 +03:30
Daniel
05a20d9279 New translations common.json (Chinese Simplified) 2025-06-25 17:54:52 +03:30
Daniel
91a5c548d8 New translations common.json (Ukrainian) 2025-06-25 17:54:51 +03:30
Daniel
95f3ae382a New translations common.json (Turkish) 2025-06-25 17:54:50 +03:30
Daniel
6e3354ff0b New translations common.json (Russian) 2025-06-25 17:54:49 +03:30
Daniel
9d92e0103c New translations common.json (Polish) 2025-06-25 17:54:48 +03:30
Daniel
a57fc3f1c2 New translations common.json (Dutch) 2025-06-25 17:54:47 +03:30
Daniel
cc3a719611 New translations common.json (Japanese) 2025-06-25 17:54:45 +03:30
Daniel
16107c8369 New translations common.json (Italian) 2025-06-25 17:54:44 +03:30
Daniel
43f6314ce2 New translations common.json (German) 2025-06-25 17:54:43 +03:30
Daniel
5884687abc New translations common.json (Spanish) 2025-06-25 17:54:42 +03:30
Daniel
835fe3de17 New translations common.json (French) 2025-06-25 17:54:40 +03:30
Daniel
f8839dbdd2 New translations common.json (Portuguese, Brazilian) 2025-06-25 17:39:39 +03:30
Daniel
b0987581c0 New translations common.json (Chinese Traditional) 2025-06-25 17:39:37 +03:30
Daniel
1a02ba64a8 New translations common.json (Chinese Simplified) 2025-06-25 17:39:36 +03:30
Daniel
0ed87d7ffc New translations common.json (Ukrainian) 2025-06-25 17:39:35 +03:30
Daniel
ddd47d74e3 New translations common.json (Turkish) 2025-06-25 17:39:33 +03:30
Daniel
213b0aafee New translations common.json (Russian) 2025-06-25 17:39:32 +03:30
Daniel
396e41a232 New translations common.json (Polish) 2025-06-25 17:39:30 +03:30
Daniel
e04c256bd3 New translations common.json (Dutch) 2025-06-25 17:39:29 +03:30
Daniel
2023d6984f New translations common.json (Japanese) 2025-06-25 17:39:28 +03:30
Daniel
43ebd72aca New translations common.json (Italian) 2025-06-25 17:39:26 +03:30
Daniel
572fa267d0 New translations common.json (German) 2025-06-25 17:39:25 +03:30
Daniel
846a233639 New translations common.json (Spanish) 2025-06-25 17:39:24 +03:30
Daniel
a6e3ae1de5 New translations common.json (French) 2025-06-25 17:39:22 +03:30
daniel31x13
c0159e5a27 minor fix 2025-06-25 09:38:16 -04:00
Daniel
ee1785ca6e Update Crowdin configuration file 2025-06-25 16:18:04 +03:30
daniel31x13
7bf4db9b25 refactor(web): use separator component 2025-06-24 14:52:09 -04:00
daniel31x13
803a97b7e0 code cleanup 2025-06-24 13:48:04 -04:00
daniel31x13
87715b8f62 revert crowdin.yml removal 2025-06-24 13:01:15 -04:00
Daniel
276a2a30c8 Update Crowdin configuration file 2025-06-24 20:29:10 +03:30
daniel31x13
bbad712e0d remove files 2025-06-24 12:51:57 -04:00
daniel31x13
32f481f65b minor fix 2025-06-24 12:34:41 -04:00
daniel31x13
b7e260e180 minor fix 2025-06-24 12:29:41 -04:00
daniel31x13
df89364c0d minor fix 2025-06-24 12:17:47 -04:00
daniel31x13
77731dcf8a minor fix 2025-06-24 12:16:05 -04:00
daniel31x13
292fadaa1e move file 2025-06-24 12:15:11 -04:00
daniel31x13
fc801f98a9 add crowdin.yml file 2025-06-24 12:10:47 -04:00
daniel31x13
a005e05d72 minor fix 2025-06-23 12:43:05 -04:00
daniel31x13
6124062a85 add crowdin file 2025-06-20 06:48:07 -04:00
Daniel
1a69058c7a Merge pull request #1226 from linkwarden/hotfix
hotfix
2025-06-12 14:38:04 +03:30
Daniel
4580f8cd26 Merge branch 'dev' into hotfix 2025-06-12 14:37:48 +03:30
Daniel
63050ae7c2 Merge pull request #1225 from linkwarden/hotfix
hotfix
2025-06-12 14:36:22 +03:30
Daniel
82f1bd943e hotfix 2025-06-12 14:35:34 +03:30
daniel31x13
6a8ce9614b fix build error 2025-06-11 11:55:47 -04:00
Daniel
60bb97e4ca Merge pull request #1223 from linkwarden/feat/import-from-pocket
feat(web): add import from pocket
2025-06-11 19:12:49 +03:30
daniel31x13
c9f2799618 feat(web): add import from pocket 2025-06-11 11:40:34 -04:00
Daniel
96b508c41a Merge pull request #1222 from linkwarden/feat/redesigned-dashboard
Feat/redesigned dashboard
2025-06-11 16:33:44 +03:30
daniel31x13
a9206ca6c9 bug fixed 2025-06-10 18:18:57 -04:00
daniel31x13
441e97e4d9 minor improvement 2025-06-10 17:46:05 -04:00
daniel31x13
a918d2c960 small change 2025-06-10 17:31:06 -04:00
daniel31x13
c9860c535b add placeholder 2025-06-10 17:28:19 -04:00
daniel31x13
c2a660fd50 better looking view 2025-06-10 17:11:42 -04:00
daniel31x13
1790638012 WIP 2025-06-10 16:32:03 -04:00
daniel31x13
c9c3941688 improvements 2025-06-10 12:55:33 -04:00
daniel31x13
9f376a633c improvements 2025-06-10 08:53:09 -04:00
daniel31x13
fa0c08a1b2 minor fix 2025-06-10 07:50:47 -04:00
daniel31x13
4e0d7f5d8e add default rows 2025-06-10 07:44:21 -04:00
Daniel
59b6b7228e Merge pull request #1216 from il516/dashboard-layout
feat(dashboard): add dashboard layout reordering
2025-06-10 14:47:22 +03:30
daniel31x13
352389958c bug fixed 2025-06-09 05:16:24 -04:00
daniel31x13
6c75f3ee58 WIP 2025-06-09 04:18:37 -04:00
daniel31x13
792d03f906 small tweaks 2025-06-08 08:46:12 -04:00
Isaac
82f4921038 fix building 2025-06-07 19:05:42 -05:00
Isaac
ff497b5f66 uncomment dropdown 2025-06-07 18:45:44 -05:00
Isaac
e012c8953c fix returned links 2025-06-07 18:42:48 -05:00
Isaac
06d8cf3cb8 format 2025-06-07 18:29:19 -05:00
Isaac
7ce78fc314 final fixes 2025-06-07 18:27:57 -05:00
Isaac
de5a43cea4 fix type 2025-06-06 23:58:41 -05:00
Isaac
6e47cc6897 Merge branch 'linkwarden:main' into dashboard-layout 2025-06-06 23:49:53 -05:00
Isaac
62b1bcbc59 fixes 2025-06-06 23:49:35 -05:00
Cory Claflin
be532d5455 Add Synology OIDC as login option based upon Authelia settings successful login 2025-06-06 22:22:35 -05:00
Isaac
8718b1acfe Merge branch 'dev' of https://github.com/IsaacWise06/linkwarden into dashboard-layout 2025-06-06 22:14:46 -05:00
Isaac
f36d36dec7 optimistic update 2025-06-06 21:44:01 -05:00
Isaac
8b6ded9179 Better drag and drop 2025-06-06 21:21:14 -05:00
Isaac
4333d2686c Improve endpoint & reordering when a collection is deleted 2025-06-06 21:06:34 -05:00
daniel31x13
78c1f6246f minor fix 2025-06-06 16:52:41 -04:00
daniel31x13
941bc04fc8 small improvement 2025-06-06 16:45:52 -04:00
daniel31x13
cfa36fc8da bug fixed 2025-06-06 16:33:47 -04:00
daniel31x13
9388073cbf bug fix 2025-06-06 16:11:54 -04:00
daniel31x13
90eeba5191 feat(web): add commenting functionality 2025-06-06 11:18:03 -04:00
daniel31x13
95846c1d09 feat(web): add comments for highlights ui 2025-06-06 10:06:40 -04:00
daniel31x13
31b8092472 small improvement 2025-06-06 08:53:33 -04:00
daniel31x13
2a62c1ee1a feat(web): added highlights drawer 2025-06-06 08:49:22 -04:00
daniel31x13
5a028e98e3 minor improvement 2025-06-05 09:06:27 -04:00
Daniel
8ed5147762 Merge pull request #1209 from Talkabout/dev
improve preview generation for webpages in case site does not provide an icon
2025-06-05 00:52:07 +03:30
Daniel
a20c4a67a8 Merge branch 'dev' into dev 2025-06-05 00:51:52 +03:30
daniel31x13
bf8ac7d801 minor fix 2025-06-04 17:16:21 -04:00
Daniel
2971871a51 Merge pull request #1196 from Vlad1mir-D/fix/952-invalid-image-url
fix(worker): determine origin for ogImageUrl (closes #952)
2025-06-05 00:17:44 +03:30
Daniel
834714a941 Merge pull request #1182 from redrathnure/fix-file-handling
fix: "Error copying file" when no src file
2025-06-04 17:24:26 +03:30
daniel31x13
08523b2234 minor fix 2025-06-04 09:52:48 -04:00
daniel31x13
479fea3ea4 minor improvement 2025-06-03 17:22:53 -04:00
daniel31x13
a34f1d7308 bug fixes 2025-06-03 17:15:21 -04:00
Daniel
b18ce2239a Merge pull request #1210 from linkwarden/feat/improved-readable-page
Feat/improved readable page
2025-06-03 23:57:19 +03:30
daniel31x13
17f32152c3 minor fix 2025-06-03 16:23:06 -04:00
daniel31x13
47c711fea6 feat(web): finished readable dropdown 2025-06-03 15:57:32 -04:00
Isaac
4423a30a72 format 2025-06-02 15:52:08 -05:00
Isaac
cefe5e9e1b Bug fixes 2025-06-02 15:47:28 -05:00
Talkabout
d30fb4645e improve preview generation for webpages in case site does not provide an icon 2025-06-02 18:08:48 +02:00
daniel31x13
ae099dce4f minor improvement 2025-06-02 03:17:46 -04:00
daniel31x13
7c740f01d2 feat(web): sleeker alert + remove collection name confirmation requirement 2025-06-02 03:12:50 -04:00
daniel31x13
2b8964ca64 feat(web): add a text style dropdown to readable view 2025-06-02 02:46:24 -04:00
Isaac
048842efa4 format 2025-06-01 17:14:28 -05:00
Isaac
29d90db991 basic endpoint, drag and drop, and ordering on dashboard 2025-06-01 17:13:54 -05:00
Isaac
c99067ac37 Start endpoint 2025-05-31 22:46:58 -05:00
Isaac
4a66e1ec9c Dropdown, database & start endpoint 2025-05-31 17:19:02 -05:00
daniel31x13
1211b6b1ef feat(web): hide navbar in readable view 2025-05-30 12:26:57 -04:00
Isaac
fbb1ce9687 another constraint 2025-05-29 23:35:09 -05:00
Isaac
3959a42a30 database 2025-05-29 23:19:22 -05:00
Isaac
759cb15148 fix 2025-05-29 23:13:23 -05:00
Isaac
8ecd35acfe Start reordering dropdown 2025-05-29 23:10:44 -05:00
daniel31x13
db9ea8eef4 improved readable view 2025-05-28 12:00:07 -04:00
daniel31x13
b32151eb7e WIP 2025-05-28 09:36:25 -04:00
daniel31x13
d377fa6eb0 minor fix 2025-05-27 21:48:01 -04:00
daniel31x13
4b958faf7e improved tabs 2025-05-27 17:58:40 -04:00
daniel31x13
6981ad6d7a refactor(web): use the new tooltip component 2025-05-27 15:20:23 -04:00
daniel31x13
2d97f3a138 small improvement 2025-05-27 12:47:33 -04:00
daniel31x13
e668b67a3d refactor(web): use the new button component for consistency and improved styling 2025-05-27 12:44:29 -04:00
Daniel
41eb7df457 Merge pull request #1202 from linkwarden/feat/improved-ui
Feat/improved UI
2025-05-26 20:02:08 +03:30
Daniel
7725a5dd8b Merge branch 'dev' into feat/improved-ui 2025-05-26 20:01:41 +03:30
daniel31x13
37588acc0d Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-05-26 12:27:43 -04:00
daniel31x13
6ad2511fbc Merge branch 'PeterDaveHelloKitchen-zh-TW' into dev 2025-05-26 12:26:17 -04:00
daniel31x13
f3cb12c962 Merge branch 'zh-TW' of https://github.com/PeterDaveHelloKitchen/linkwarden into PeterDaveHelloKitchen-zh-TW 2025-05-26 12:25:30 -04:00
Daniel
99fa0b1d8b Merge pull request #1188 from SoCuul/fix/name-capitalization
fix(web): fix capitalization of collection names
2025-05-26 19:49:35 +03:30
Daniel
9db7ff329a Merge pull request #1201 from il516/bulk-refresh-preservations
feat(links): Add refresh preserved formats to bulk actions
2025-05-26 19:47:32 +03:30
daniel31x13
6bdf0f1c91 small change 2025-05-26 12:16:21 -04:00
Isaac
cd23f10480 format 2025-05-25 15:11:20 -05:00
Isaac
bea11edbe8 tooltip 2025-05-25 15:09:36 -05:00
Isaac
d565958e5f Fix permissions 2025-05-24 22:47:33 -05:00
daniel31x13
7222b19745 minor change 2025-05-24 17:05:32 -04:00
daniel31x13
b9be960c7e improved dropdown UI 2025-05-24 17:01:15 -04:00
Isaac
b337045ab2 Start bulk refreshing preserved formats 2025-05-23 21:23:07 -05:00
Vladimir
14db14ff07 fix(worker): determine origin for ogImageUrl (closes #952) 2025-05-24 02:20:04 +03:00
daniel31x13
6ca2774b28 replaced button component with shadcn button 2025-05-23 18:49:25 -04:00
daniel31x13
c3d702ad53 minor fix 2025-05-23 18:07:40 -04:00
daniel31x13
921d8a3718 base comment for shadcn 2025-05-23 17:42:48 -04:00
daniel31x13
2b258680b1 minor improvement to the dropdown component 2025-05-23 16:58:56 -04:00
daniel31x13
82f5a7026f feat(web): make things less rounded 2025-05-23 15:52:46 -04:00
daniel31x13
daef130728 feat(mobile): add modal component 2025-05-23 10:23:38 -04:00
daniel31x13
a181b43529 refactor(mobile): use nativewind instead of regular styles 2025-05-22 11:57:33 -04:00
Daniel
968bd04b40 Merge pull request #1194 from linkwarden/hotfix
minor fix
2025-05-22 13:31:52 +03:30
Daniel
46c20af530 fix logo 2025-05-22 13:30:00 +03:30
daniel31x13
cfb4081112 minor fix 2025-05-22 05:55:55 -04:00
daniel31x13
61ca5cf3e5 feat(mobile): add persistence 2025-05-19 20:14:50 -04:00
daniel31x13
cecbf694f5 fix(mobile): replace react-native-render-html package 2025-05-14 13:36:24 -04:00
daniel31x13
0e122c7485 feat(mobile): view readable formats 2025-05-14 09:36:55 -04:00
daniel31x13
c6713f67f4 feat(mobile): preserved formats basic viewing functionality 2025-05-12 12:02:31 -04:00
daniel31x13
dc8e763b76 feat(mobile): add search functionality 2025-05-12 06:27:10 -04:00
daniel31x13
2a52a0c79f fix(web): improved prompt output 2025-05-12 04:09:54 -04:00
SoCuul
08e1f499e1 fix(web): fix capitalization of collection names 2025-05-10 00:52:32 -07:00
Peter Dave Hello
63196e7b99 Update and improve zh-TW Traditional Chinese locale 2025-05-07 22:34:28 +08:00
Maxim Medvedev
7cae35ce8d fix: "Error copying file" when no src file 2025-05-06 18:52:06 +02:00
daniel31x13
369b3d6207 fix(web): active tabs now are adjusted properly 2025-05-06 08:51:10 -04:00
Daniel
3762d971e9 Merge pull request #1181 from linkwarden/dev
minor fix
2025-05-06 06:40:59 +03:30
daniel31x13
f3d8a3cc95 minor fix 2025-05-05 23:09:33 -04:00
Daniel
56efeecbfd Merge pull request #1180 from linkwarden/dev
v2.10.2
2025-05-06 06:08:22 +03:30
Daniel
be8aef987a Merge pull request #1174 from wnor543/dev
feat(web): add support for http/https proxies
2025-05-06 06:03:47 +03:30
daniel31x13
4b9de5cd96 add https-proxy-agent to worker as well 2025-05-05 22:33:11 -04:00
daniel31x13
59949e21ee bump version 2025-05-05 22:17:34 -04:00
Daniel
6fda674529 Merge pull request #1179 from linkwarden/dev
fix(worker): prevent sendToWayback from crashing
2025-05-06 02:49:50 +03:30
daniel31x13
4318207c39 fix(worker): prevent sendToWayback from crashing 2025-05-05 19:18:56 -04:00
Daniel
c48ad18379 Merge pull request #1178 from linkwarden/dev
Dev
2025-05-06 02:40:50 +03:30
daniel31x13
d647c60f04 fix(worker): exit monolith processes appropriately 2025-05-05 19:08:52 -04:00
wnor543
f83a9e899c feat(web): add support for http/https proxies 2025-05-05 11:01:45 +00:00
daniel31x13
b7c0cef8de feat(mobile): open saved items in webview 2025-05-04 13:40:43 -04:00
daniel31x13
14340afb99 code cleanup 2025-05-04 12:01:24 -04:00
Daniel
b903a12f8d Merge pull request #1171 from linkwarden/dev
minor fix
2025-05-03 19:14:04 +03:30
daniel31x13
c6f7c18441 minor fix 2025-05-03 11:42:36 -04:00
Daniel
6d334be82e Merge pull request #1170 from linkwarden/dev
v2.10.1
2025-05-03 18:14:02 +03:30
daniel31x13
195cb99c90 log processing queue to console 2025-05-03 09:10:39 -04:00
daniel31x13
2ebd311e0e small fix 2025-05-03 09:10:10 -04:00
Daniel
1da1f17ea3 Merge pull request #1168 from simcop2387/main
Implement more of the vercel openai sdk bits to allow for api compatible servers
2025-05-03 16:02:59 +03:30
daniel31x13
bc36513952 used the "@ai-sdk/openai-compatible" package instead 2025-05-03 08:15:13 -04:00
daniel31x13
b04ab898d7 bug fix 2025-05-02 11:31:01 -04:00
Ryan Voots
778dd764f6 Merge remote-tracking branch 'upstream/dev' 2025-05-02 09:11:50 -04:00
daniel31x13
64f8922741 bump meilisearch timeout and make it configurable 2025-05-02 00:28:21 -04:00
Ryan Voots
c8ed9ac72d Implement more of the vercel openai sdk bits to allow for api compatible proxies and servers 2025-05-01 22:15:08 -04:00
daniel31x13
6cfd26da32 bug fix 2025-05-01 05:48:50 -04:00
daniel31x13
b56cf3faa4 bug fix 2025-05-01 05:33:59 -04:00
daniel31x13
f7526df008 bug fix 2025-04-30 11:52:14 -04:00
daniel31x13
bbeba7f50f improved mobile app + fixed a bug in the worker script 2025-04-30 10:16:24 -04:00
daniel31x13
7e5fa3eacd minor improvement 2025-04-27 11:52:39 -04:00
daniel31x13
89826bd721 add loader 2025-04-27 11:21:34 -04:00
daniel31x13
fb819b6142 ui fix 2025-04-27 09:51:39 -04:00
daniel31x13
2e131403d4 add LinkListing component 2025-04-27 09:43:02 -04:00
daniel31x13
83db594fde bug fixed 2025-04-27 01:31:18 -04:00
daniel31x13
83b002d585 remove old readme 2025-04-24 06:07:36 -04:00
Daniel
7207259cdf Merge pull request #1155 from linkwarden/mobile-app
Mobile-app
2025-04-24 12:09:51 +03:30
daniel31x13
7c647ae02d bug fix 2025-04-23 16:44:13 -04:00
daniel31x13
0630ea536e bug fixed 2025-04-23 15:54:21 -04:00
daniel31x13
f2f73fc894 bug fixed 2025-04-23 10:49:11 -04:00
daniel31x13
7887f55dd0 bug fix 2025-04-21 20:04:16 -04:00
daniel31x13
5ba759bb41 base commit for mobile app 2025-04-21 19:05:49 -04:00
daniel31x13
f325be0364 remove old architecture file 2025-04-21 12:55:14 -04:00
daniel31x13
f3c8647ff2 move react-query to the @linkwarden/router package 2025-04-21 10:14:08 -04:00
daniel31x13
ceb6b8f8e7 add router directory 2025-04-21 09:28:59 -04:00
daniel31x13
94d953d449 adapt docker to the new folder structure + bug fix 2025-04-19 17:48:01 -04:00
daniel31x13
7f74fd75c9 made everything functional across the workspaces 2025-04-19 10:48:22 -04:00
daniel31x13
4151b37f9f separate prisma as a package 2025-04-14 13:26:55 -04:00
daniel31x13
1b45286aaf move files to /apps/web directory 2025-04-10 18:38:59 -04:00
Daniel
d578fdc0c4 Merge pull request #1104 from linkwarden/dev
v2.10.0
2025-04-08 00:42:32 +03:30
Daniel
2f2747cfc8 Merge pull request #1083 from sinsky/openrouter-provider
OpenRouter AI Provider
2025-04-05 16:59:46 +03:30
Daniel
f254bd85b5 Merge pull request #1109 from TheMeier/gitlab_url
feat(gitlab-auth): allow to configure GitLab instance
2025-04-04 21:51:53 +03:30
daniel31x13
a321d12307 small fix 2025-04-04 14:02:32 -04:00
daniel31x13
0e5e3ea00e finished highlight feature implementation 2025-04-04 13:39:54 -04:00
daniel31x13
6599fd7f5a minor fix 2025-04-04 11:29:46 -04:00
daniel31x13
225288c742 minor fix 2025-04-04 11:29:17 -04:00
daniel31x13
9bf77e849f minor improvement 2025-04-04 11:16:10 -04:00
daniel31x13
44ae6d0dbf WIP 2025-04-01 02:17:00 -04:00
Christoph Maser
3684204a03 feat(gitlab-auth): allow to configure GitLab instance
This change allows to configure the GitLab instance URL in the
`.env` file. This is useful for self-hosted GitLab instances.
2025-03-30 16:49:29 +02:00
daniel31x13
5d17b628cc small change 2025-03-20 23:41:24 -04:00
daniel31x13
197a5b3b74 bug fix 2025-03-20 23:25:32 -04:00
daniel31x13
278b674ea7 small change 2025-03-20 08:00:50 -04:00
daniel31x13
78e8078d6b upped the size limits for the preservations 2025-03-20 00:11:04 -04:00
daniel31x13
2f05dbad5d improvements to docker 2025-03-19 23:47:31 -04:00
daniel31x13
0987475e41 revert change 2025-03-19 11:56:42 -04:00
daniel31x13
41e34ba4ec small improvement 2025-03-19 11:52:43 -04:00
daniel31x13
dd78c1570f bug fix 2025-03-18 09:20:40 -04:00
daniel31x13
5bf90c56de bug fix 2025-03-18 08:33:24 -04:00
daniel31x13
35643faf85 bug fixed 2025-03-12 10:36:47 -04:00
Daniel
72c5a05324 Merge pull request #1093 from linkwarden/feat/text-highlighting
Feat/text highlighting
2025-03-12 17:34:49 +03:30
daniel31x13
0de7988b29 update highlight functionality 2025-03-11 20:12:30 -04:00
daniel31x13
43d5f0a205 remove highlight client side functionality 2025-03-11 19:57:26 -04:00
daniel31x13
d703ff072c add remove highlight server side 2025-03-11 09:22:47 -04:00
daniel31x13
6f80fa62d2 view highlights + bug fixed 2025-03-10 22:46:56 -04:00
daniel31x13
34b914b91f clear comments 2025-03-10 17:02:31 -04:00
daniel31x13
c6c8dab5db post highlight functionality 2025-03-09 14:42:06 -04:00
sinsky
d4bbdebe31 added openrouter ai provider 2025-03-07 02:09:36 +09:00
daniel31x13
2f5c431fa7 small improvement 2025-03-04 08:56:14 -05:00
daniel31x13
6d92ce64bd bug fix 2025-03-04 08:48:45 -05:00
daniel31x13
6c006bb748 bug fix 2025-03-04 08:42:03 -05:00
daniel31x13
d2cb7604fa improved preservation page 2025-03-04 08:29:12 -05:00
daniel31x13
dd061e9dc8 wip 2025-03-04 07:06:22 -05:00
daniel31x13
63c50d96d7 small improvement 2025-03-03 22:47:50 -05:00
daniel31x13
1360a03eb5 revert modal 2025-03-03 18:01:02 -05:00
daniel31x13
902c724f39 remove tiptap 2025-03-03 17:22:57 -05:00
daniel31x13
1677e5e0ab bug fixed 2025-03-02 11:56:40 -05:00
daniel31x13
1989510aac Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2025-03-02 05:55:52 -05:00
daniel31x13
ef3cf4bcfa add line to .env.sample 2025-03-02 05:55:46 -05:00
Daniel
645df6c0aa Merge pull request #1069 from weikinhuang/remote-playwright
Add PLAYWRIGHT_WS_URL to connect to a remote chrome instance
2025-03-02 14:24:56 +03:30
Daniel
dfad34c3dd Merge pull request #1042 from clemenstyp/azure-ai-provider
Azure AI provider
2025-03-02 13:46:37 +03:30
Daniel
f5abe4e1a2 Merge pull request #1067 from sur5r/linkcounttitlefix
Fix collection card link count title
2025-03-02 13:42:57 +03:30
Daniel
79e447c58d Merge pull request #1076 from linkwarden/feat/meilisearch-implementation
Feat/meilisearch implementation
2025-03-02 13:38:49 +03:30
daniel31x13
bc013e7819 implement the meilisearch for the public page 2025-02-28 06:02:41 -05:00
Wei Kin Huang
12844f2529 Add PLAYWRIGHT_WS_URL to connect to a remote chrome instance 2025-02-26 20:00:25 -05:00
daniel31x13
92a66933ce remove unused code 2025-02-26 09:51:54 -05:00
daniel31x13
bc01b6d4a9 remove filter dropdown 2025-02-26 09:18:22 -05:00
Jakob Haufe
23650bb684 Fix collection card link count title
The link icon on the collection card has a wrong title which is even
misleading if the collection is NOT shared publicly.

This was originally introduced in cf1306d2c4
and seemingly never noticed.
2025-02-25 11:21:14 +01:00
daniel31x13
140ee8c65d bug fix 2025-02-22 16:53:56 -05:00
daniel31x13
7c153d841a bug fix 2025-02-22 16:38:59 -05:00
daniel31x13
e06f52642a improvements 2025-02-22 11:18:53 -05:00
daniel31x13
874a909d1c minor improvement 2025-02-22 10:28:01 -05:00
daniel31x13
bbe702925b add negative queries 2025-02-22 10:22:42 -05:00
daniel31x13
49262d52cf bug fix 2025-02-22 07:52:23 -05:00
daniel31x13
5a468b44a1 wip 2025-02-22 07:19:48 -05:00
daniel31x13
6678f9c971 bump timeout 2025-02-16 13:22:39 -05:00
daniel31x13
b5a27968de bug fix 2025-02-16 12:43:51 -05:00
daniel31x13
4f1c6855aa bug fix 2025-02-16 12:11:43 -05:00
daniel31x13
50c7fcd012 bug fix 2025-02-16 11:23:03 -05:00
daniel31x13
de63ba523e small change 2025-02-16 11:16:13 -05:00
daniel31x13
5e2362ea62 bug fixed 2025-02-16 11:12:37 -05:00
daniel31x13
db94b01859 update take logic 2025-02-16 09:58:55 -05:00
daniel31x13
9bd821baa9 refactored search 2025-02-15 17:14:26 -05:00
daniel31x13
b69b535155 add package 2025-02-11 05:54:41 -05:00
Clemens Eyhoff
0bee36eced added azure ai provider 2025-02-11 10:55:05 +01:00
daniel31x13
d044a6cbba pass version to config route 2025-02-10 05:07:52 -05:00
Daniel
8095010835 Merge pull request #1039 from il516/worker
feat(archival): Implement archival tags and support deletion/updating of existing preservations
2025-02-09 21:03:41 +03:30
daniel31x13
bd21c3d8c3 more improved UI 2025-02-09 12:32:38 -05:00
daniel31x13
2bacf6e07d minor improvements 2025-02-09 10:40:20 -05:00
Isaac
5c73d7097f don't need this 2025-02-08 22:32:25 -06:00
Isaac
f77236d49e FIX 2025-02-08 22:29:13 -06:00
Isaac
1152571e49 format 2025-02-08 22:20:28 -06:00
Isaac
eda41d7132 Fix console warning 2025-02-08 22:20:16 -06:00
Isaac
dd58a8565a more fixes 2025-02-08 22:14:18 -06:00
Isaac
64d20df6f1 Fix tag selection & deleting new tags 2025-02-08 21:47:51 -06:00
Isaac
cd3f5f7e70 revert 2025-02-08 21:13:12 -06:00
Isaac
fca04dd0a0 fix building 2025-02-08 21:11:01 -06:00
Isaac
e024b324ed format 2025-02-08 20:57:14 -06:00
Isaac
f012eaee33 move api route & update schema 2025-02-08 20:56:36 -06:00
Isaac
794f8f07fa use existing TagSelection 2025-02-08 20:26:53 -06:00
Isaac
0a9aa774cc use existing modal for confirmation modal 2025-02-08 20:09:36 -06:00
Isaac
ebbc23f581 Make sure the user has an AI tagging method set 2025-02-07 20:39:01 -06:00
Isaac
bf7722aa2e format 2025-02-07 17:45:14 -06:00
Isaac
33637a8bca Regenerate preview when choosing to represerve 2025-02-07 17:44:26 -06:00
Isaac
80962c5df6 format & lint 2025-02-07 14:58:01 -06:00
Isaac
7fa9621de1 Merge branch 'dev' into worker 2025-02-07 14:52:30 -06:00
Isaac
d17fed9c85 Finish delete/update preservations 2025-02-07 14:45:18 -06:00
Isaac
81536e61f7 Update text & add third option 2025-02-06 21:32:53 -06:00
Isaac
c790781315 Start worker archival update/delete options 2025-02-06 21:21:18 -06:00
Isaac
e7a067b358 Verify user is admin 2025-02-06 18:06:01 -06:00
Isaac
4156126a71 add archival delete/update route 2025-02-06 18:04:52 -06:00
Isaac
95f456bbb6 Add tag archival settings to the worker 2025-02-06 17:34:49 -06:00
Isaac
102cd1a6cf Fix endpoint & useMutation callback 2025-02-06 16:49:26 -06:00
Isaac
395dc5c0cb Track changes to prevent useless requests 2025-02-06 16:20:19 -06:00
Isaac
7f0c8f4bbf finish endpoint 2025-02-05 18:48:41 -06:00
Isaac
671f27ccde fix deleting archival tags 2025-02-05 17:46:36 -06:00
Isaac
c90b53376d Archival tags post route 2025-02-05 16:47:16 -06:00
daniel31x13
13ed7b6cdc bug fixed 2025-02-05 06:49:38 -05:00
Daniel
3c71301cbc Merge pull request #1021 from stuzer05/dev
Add collection select sorting, fix inconsistent sidebar reorder
2025-02-05 15:15:40 +03:30
daniel31x13
833a871731 remove extra logging 2025-02-05 05:56:54 -05:00
daniel31x13
7b3a4e48c1 remove note modal to focus on advanced bookmarking functionality 2025-02-05 05:50:18 -05:00
Isaac
76112534f6 remove duplicate functions 2025-02-04 17:06:43 -06:00
Isaac
adafe36cd7 Merge branch 'worker' of https://github.com/il516/linkwarden into worker 2025-02-04 16:58:00 -06:00
Isaac
0123791b8a use a hook to manage state for archival tags input 2025-02-04 16:51:56 -06:00
Isaac
81f150c0ce Merge branch 'linkwarden:main' into worker 2025-02-04 01:50:12 -06:00
Isaac
9177ad6a72 complete toggling options 2025-02-03 21:53:16 -06:00
Isaac
d360f612e2 add ai tagging & fix select 2025-02-03 21:13:41 -06:00
Daniel
b0496e2e65 Merge pull request #942 from AverageHelper/avg/decode-html-entities
[Fix] Work around parser bug that mangles attribute values
2025-02-03 19:35:28 +03:30
daniel31x13
c5751386fa undo changes to yarn.lock 2025-02-03 11:05:10 -05:00
Daniel
406647d687 Merge pull request #1003 from Kur0den/dev
Fix mistranslation and add new translation in Japanese
2025-02-03 18:11:46 +03:30
Daniel
5949a0965c Merge pull request #1017 from YeeJiaWei/fix/double-overflow
fixed height causing double overflow
2025-02-03 18:01:24 +03:30
daniel31x13
c015bed8a7 small improvement 2025-02-03 09:30:52 -05:00
daniel31x13
504af53f18 bug fix 2025-02-03 08:26:25 -05:00
daniel31x13
d822db440b improved UX 2025-02-03 08:22:18 -05:00
daniel31x13
181e1009e5 fix build errors 2025-02-03 07:47:46 -05:00
Daniel
db16044949 Merge pull request #1028 from il516/more-ai-providers
feat(tags): Ability to use OpenAI/Anthropic & auto tag existing links
2025-02-03 16:09:03 +03:30
daniel31x13
1626a277e0 better UX 2025-02-03 07:38:16 -05:00
daniel31x13
616efffed2 improvements 2025-02-03 06:57:22 -05:00
daniel31x13
c9dd143d59 remove extra logging 2025-02-03 04:14:15 -05:00
Isaac
0a4d62491d Start tag archival options 2025-02-02 15:59:37 -06:00
daniel31x13
9e417b7f35 bug fixed 2025-02-02 11:30:29 -05:00
daniel31x13
81e9b27683 small change 2025-02-02 03:09:20 -05:00
Isaac
942ae4af99 format 2025-02-01 18:03:26 -06:00
Isaac
f7e9119450 Remove comment 2025-02-01 18:03:10 -06:00
Isaac
d71216a908 Add user option to archive as readable 2025-02-01 18:02:12 -06:00
Isaac
f61ce5563d Max tag name length 2025-02-01 17:10:31 -06:00
Isaac
575d91832e format 2025-02-01 17:07:56 -06:00
Isaac
15fe4575f9 Use existing tags for ai tagging 2025-02-01 17:04:54 -06:00
daniel31x13
9ba9b06ae3 remove comments 2025-02-01 06:27:14 -05:00
daniel31x13
cf16344aef add note modal 2025-02-01 06:25:39 -05:00
Isaac
29ed07c74a format 2025-01-29 17:25:52 -06:00
Isaac
493e0e2f6b Merge branch 'more-ai-providers' of https://github.com/il516/linkwarden into more-ai-providers 2025-01-29 17:04:52 -06:00
Isaac
d76d99844c remove console log 2025-01-29 17:04:28 -06:00
Isaac
533f29706e Merge branch 'linkwarden:main' into more-ai-providers 2025-01-29 15:28:14 -06:00
Isaac
88cf45c7c2 format 2025-01-29 14:12:48 -06:00
Isaac
b100129d80 use env interval 2025-01-29 14:12:10 -06:00
Isaac
a267d4ed3a format 2025-01-29 14:09:15 -06:00
Isaac
9d8d5f0fa0 auto tag existing links & use meta description 2025-01-29 14:08:34 -06:00
daniel31x13
aa6b068d92 remove unused imports 2025-01-29 04:37:08 -05:00
daniel31x13
4b230e01d3 fix typo 2025-01-29 03:01:13 -05:00
daniel31x13
d140e2109f wip 2025-01-29 02:44:13 -05:00
Isaac
d5703ba70e Fix ollama 2025-01-28 20:01:56 -06:00
Isaac
5f12046a49 Provide list of models for openai & anthropic 2025-01-28 19:26:52 -06:00
Isaac
edad55d608 use provided ollama URL 2025-01-28 19:22:00 -06:00
Isaac
fac46de09c Add OpenAI & Anthropic 2025-01-28 19:19:16 -06:00
stuzer05
007de56cd3 Commit 2025-01-27 10:45:33 +02:00
stuzer05
8efdf6d87b Commit 2025-01-27 10:34:23 +02:00
daniel31x13
69225e0642 minor improvement 2025-01-27 02:40:13 -05:00
daniel31x13
0aa23e27b3 minor fix 2025-01-27 02:16:23 -05:00
daniel31x13
3bf2daddd1 improvements 2025-01-27 02:08:12 -05:00
stuzer05
71644e6e4e Commit 2025-01-27 08:55:51 +02:00
daniel31x13
6e7f92c046 improvements 2025-01-27 00:59:57 -05:00
daniel31x13
8d4504262b small fix 2025-01-27 00:34:36 -05:00
daniel31x13
20224c835a small improvement 2025-01-27 00:25:19 -05:00
daniel31x13
c3873b030f full width expanded mode 2025-01-27 00:20:17 -05:00
daniel31x13
cf8a202afa small fix 2025-01-27 00:05:34 -05:00
daniel31x13
c79b1e6492 cleaner code 2025-01-27 00:04:33 -05:00
daniel31x13
e21e3ecaae minor change 2025-01-26 14:12:39 -05:00
daniel31x13
b84840e12c wip 2025-01-25 11:35:01 -05:00
daniel31x13
f8a130ae6e add component 2025-01-25 10:11:34 -05:00
daniel31x13
852de0d587 minor fix 2025-01-24 18:00:50 -05:00
daniel31x13
eecfb112e3 small change 2025-01-24 17:45:01 -05:00
daniel31x13
81a35655e9 add portal wrapper component 2025-01-24 17:28:29 -05:00
Yee Jia Wei
c45b44cdbc fixed height causing double overflow 2025-01-25 02:56:35 +08:00
daniel31x13
ecc48f8fe2 wip 2025-01-24 06:10:44 -05:00
daniel31x13
e1c4f85e53 small fix 2025-01-22 04:01:22 -05:00
daniel31x13
f8c96b493c wip 2025-01-22 03:49:16 -05:00
daniel31x13
63904f6d41 small fix 2025-01-19 12:37:32 -05:00
Daniel
9083d9a01b Merge pull request #1008 from linkwarden:patch
add issue template
2025-01-19 10:53:51 -05:00
Daniel
806cba8110 Merge pull request #1007 from linkwarden/patch
add issue template
2025-01-19 10:51:50 -05:00
daniel31x13
ae24358a77 add issue template 2025-01-19 10:50:44 -05:00
daniel31x13
f0dfd5568e small fix 2025-01-18 16:19:20 -05:00
daniel31x13
9d17600124 bug fix 2025-01-18 15:58:58 -05:00
daniel31x13
6a72d7894b bug fixed 2025-01-18 00:56:31 -05:00
daniel31x13
00c33d48f0 wip 2025-01-18 00:36:55 -05:00
daniel31x13
6573c683f6 support for markdown for readable content 2025-01-15 22:11:26 -05:00
Kur0den0010
5412545c6f Change translate in Japanese
Change start_journey
2025-01-16 10:54:18 +09:00
Kur0den0010
eee06e2be9 Add new translation in Japanese
Add invalid_url_guide to search_query_invalid_symbol
2025-01-16 10:52:01 +09:00
Kur0den0010
c3e8097aac Add missing translation keys in Japanese
Add invalid_url_guide to search_query_invalid_symbol
2025-01-16 10:45:16 +09:00
Kur0den0010
2d97feef17 Add missing translation in Japanese
Add from_omnivore
2025-01-16 10:43:42 +09:00
Kur0den0010
74c0a40622 Fix mistranslation in Japanese
Change created
2025-01-16 10:42:06 +09:00
daniel31x13
56741b123b minor fix 2025-01-15 19:02:06 -05:00
daniel31x13
e8c6cc45f4 minor fix 2025-01-15 18:33:22 -05:00
daniel31x13
d0c999655c improved preservation view 2025-01-15 18:32:29 -05:00
daniel31x13
05594e6507 redesigned preserved view 2025-01-15 12:10:17 -05:00
daniel31x13
b59663ea91 WIP 2025-01-15 05:56:11 -05:00
daniel31x13
8d2029a19d wip 2025-01-15 05:04:21 -05:00
daniel31x13
cee2f0a759 WIP 2025-01-15 02:57:27 -05:00
daniel31x13
032f96191e WIP 2025-01-15 02:33:39 -05:00
Daniel
e87bfc83bf Merge pull request #996 from linkwarden/dev
v2.9.3
2025-01-13 11:17:32 -05:00
daniel31x13
aa0c7d64f4 bug fixes 2025-01-13 11:12:41 -05:00
Daniel
b8a1839fb6 Merge pull request #972 from siberianspot/main
Localization and small fixes
2025-01-12 18:46:32 -05:00
Daniel
629574c6b2 Merge branch 'dev' into main 2025-01-12 18:45:51 -05:00
daniel31x13
1bb7d3cc5c fixes #981 2025-01-11 09:47:49 -05:00
daniel31x13
6b811c3e7d undo 2025-01-11 09:44:27 -05:00
daniel31x13
a941eec569 bug fix 2025-01-11 08:47:54 -05:00
Daniel
5ed79f0f5b Merge pull request #990 from linkwarden/dev
update README
2025-01-10 15:09:26 -05:00
daniel31x13
010ca8eeae update README 2025-01-10 14:59:07 -05:00
Denis Bryukhanov
5840ffc620 Merge pull request #1 from linkwarden/main
Merge with latest
2025-01-08 01:53:44 +07:00
siberian
14754a23f1 Adding missing localizations, Fix Russian localization, Correcting a duplicate value in the settings 2025-01-08 01:50:21 +07:00
Daniel
1a501b5365 Merge pull request #967 from linkwarden/dev
Dev
2025-01-07 08:28:11 -05:00
daniel31x13
3bc9bbf074 bump version 2025-01-07 08:26:37 -05:00
daniel31x13
09a52dd260 bug fixed 2025-01-07 08:26:04 -05:00
Daniel
84e99b55c9 Merge pull request #959 from linkwarden/dev
revert variable name
2025-01-06 13:32:38 -05:00
daniel31x13
22c4fbf613 revert variable name 2025-01-06 13:32:02 -05:00
Daniel
0e1b51177b Merge pull request #958 from linkwarden/dev
v2.9.1
2025-01-06 13:28:45 -05:00
daniel31x13
44499c1277 bump version 2025-01-06 13:28:25 -05:00
daniel31x13
9d986356a7 switch to react-query for fetching the config 2025-01-06 12:56:17 -05:00
daniel31x13
1d854e16aa bug fix 2025-01-06 12:39:16 -05:00
daniel31x13
02c02fc3b9 use SSR for public ENV 2025-01-06 11:32:44 -05:00
Daniel
99bdc7d55e Merge pull request #954 from linkwarden/dev
bug fixed
2025-01-06 05:44:35 -05:00
daniel31x13
62c7bbbb74 bug fixed 2025-01-06 05:43:22 -05:00
Daniel
dfb31ab1b3 Merge pull request #948 from linkwarden/dev
Linkwarden v2.9.0
2025-01-06 05:09:24 -05:00
daniel31x13
e0c0b76eb0 update README 2025-01-05 19:25:00 -05:00
daniel31x13
9bc261bc85 minor change 2025-01-02 23:16:03 -05:00
daniel31x13
b2d2e23539 fix: better green 2025-01-02 16:37:07 -05:00
daniel31x13
04c69bb05f feat: add option to open link details 2025-01-02 16:33:58 -05:00
daniel31x13
0cc1fd8407 WIP 2025-01-02 16:26:38 -05:00
daniel31x13
848e0bf50e feat: quick look at the preserved formats 2025-01-02 15:42:47 -05:00
daniel31x13
c3981c7fff feat: support for multiple themes 2025-01-02 13:48:35 -05:00
daniel31x13
ec7d6f4a6b minor improvement 2024-12-31 18:43:11 -05:00
daniel31x13
9d8b602839 feat: choose what to show in dashboard 2024-12-31 18:42:34 -05:00
daniel31x13
f0f57fb1a9 bug fix 2024-12-31 17:01:27 -05:00
daniel31x13
88820361e9 update README 2024-12-31 08:36:06 -05:00
daniel31x13
be47c78e4d small improvement to the public collection page 2024-12-30 06:05:15 -05:00
daniel31x13
fa059d1b00 bug fixed 2024-12-30 04:15:43 -05:00
daniel31x13
bcfec38adf bug fixed 2024-12-30 04:07:48 -05:00
AverageHelper
0344467cb7 feat: Use the same decoder that JSDom uses to encode 2024-12-29 01:37:22 -07:00
daniel31x13
454ed7b7eb more efficient docker-compose file 2024-12-29 02:04:28 -05:00
Daniel
51da37a22f Merge pull request #893 from zodac/dev
Adding healthcheck to docker image
2024-12-29 01:43:00 -05:00
Daniel
6edbc4f438 Merge pull request #902 from keizie/fix/import-pin
fix(import): save pinnedLinks from Linkwarden export json
2024-12-29 01:17:00 -05:00
AverageHelper
899ddafd90 fix(import-html): Work around parser bug that mangles attribute values 2024-12-28 23:11:50 -07:00
Daniel
755721f1c2 Merge pull request #920 from Zalaxx/dev
fix: HTML backup with Monolith when using Docker
2024-12-29 01:04:46 -05:00
Daniel
0b20f61913 Merge pull request #928 from il516/rss-feeds
feat(rss): Add RSS feeds for collections
2024-12-25 08:08:00 -05:00
daniel31x13
e9cf93d769 minor improvement 2024-12-25 08:07:15 -05:00
Daniel
f94d10a3d3 Merge pull request #934 from mrkhachaturov/main
russian language support
2024-12-24 09:31:34 -05:00
Daniel
6a95f6efdc Merge branch 'dev' into main 2024-12-24 09:30:19 -05:00
Daniel
150eeb9f11 Merge pull request #875 from dereulenspiegel/omnivore-import
Omnivore import
2024-12-24 09:27:48 -05:00
daniel31x13
fe77625289 finalized the implementation 2024-12-24 09:22:53 -05:00
zodac
63e7377df4 Installing curl 2024-12-24 21:48:16 +13:00
Ruben Khachaturov
458eae9a3c russian language support 2024-12-23 08:42:48 +03:00
daniel31x13
7ef2afae7f small change 2024-12-22 14:07:10 -05:00
Isaac Wise
a1d02f110d Fix RSS Subscription form & move endpoint 2024-12-19 17:16:07 -06:00
Isaac Wise
378fec06bb Format & Lint 2024-12-19 16:06:24 -06:00
Isaac Wise
e04997c8c4 Add RSS Feed button 2024-12-19 16:05:34 -06:00
Isaac Wise
aecb90b4a4 use correct protocol 2024-12-19 01:02:56 -06:00
Isaac Wise
dac6ec966c rss feeds 2024-12-19 00:44:53 -06:00
daniel31x13
582159f454 resolved conflicts 2024-12-14 10:28:46 -05:00
dereulenspiegel
cf02b7a099 Rebased to dev and ran formatting 2024-12-13 18:18:32 +01:00
dereulenspiegel
7b4d324852 Previous import mechanism is restored to previous behavior, while omnivore uses new binary import path 2024-12-13 18:17:57 +01:00
dereulenspiegel
8932b9929a Moving omnivore import to specific binary upload route 2024-12-13 18:17:56 +01:00
dereulenspiegel
6b416f23f0 Now limiting the read size for imports in a configuration compatible way to the previous implementation 2024-12-13 18:17:55 +01:00
dereulenspiegel
fb25cf5c75 Adopted other import functions to handle streams instead of text 2024-12-13 18:17:54 +01:00
dereulenspiegel
c8f33f4800 Returning 200 success to avoid stalled responses 2024-12-13 18:17:53 +01:00
dereulenspiegel
95f818959b Made the import links dropdown menu reusable to avoid code duplication 2024-12-13 18:17:52 +01:00
dereulenspiegel
a0aabde322 Fixed the unnecessary creation of multiple import collections 2024-12-13 18:17:51 +01:00
dereulenspiegel
0e3ca5b51f Made importBookmarks reusable 2024-12-13 18:17:46 +01:00
dereulenspiegel
a733cc69a3 Import should now work on testdata 2024-12-13 18:17:03 +01:00
dereulenspiegel
cbc88ebcb2 Revert "Added adm-zip dependency to be able to handle zip files for importing omnivore exports"
This reverts commit 47367c44c1.
2024-12-13 18:17:02 +01:00
dereulenspiegel
c3b78a8f82 Added adm-zip dependency to be able to handle zip files for importing omnivore exports 2024-12-13 18:17:00 +01:00
dereulenspiegel
8ad9ab7755 Added basic infrastructure to be able to have omnivore imports 2024-12-13 18:16:58 +01:00
daniel31x13
4bf220c786 minimize the usage of external dependencies 2024-12-11 09:36:42 -05:00
Daniel
ed8f2d3777 Merge pull request #894 from KittyKatt/dev
Sub-collections included in collection page and total link count
2024-12-10 07:15:35 -05:00
daniel31x13
5b87799fdd improvements 2024-12-10 07:14:17 -05:00
Daniel
97abe5de0c Merge branch 'dev' into omnivore-import 2024-12-10 06:25:01 -05:00
daniel31x13
731b259329 update version number 2024-12-09 23:48:41 -05:00
daniel31x13
63cef8e6b0 minor fix 2024-12-09 22:38:51 -05:00
Daniel
18db677c29 Merge pull request #921 from linkwarden/ai-tagging
Ai tagging
2024-12-09 22:36:20 -05:00
Daniel
6071aa617f Merge branch 'dev' into ai-tagging 2024-12-09 22:33:03 -05:00
daniel31x13
f270adbffa fully implemented ai tagging 2024-12-09 22:31:39 -05:00
daniel31x13
6259048431 remove pending format logic 2024-12-09 16:17:39 -05:00
daniel31x13
40f4a5acd9 cleaner code + add "aiTagged" field 2024-12-09 14:10:20 -05:00
Zalax
4240d37d77 Merge branch 'linkwarden:dev' into dev 2024-12-09 19:07:11 +00:00
Zalax
0abe065c0c fix: HTML backup with Monolith when using Docker
ca-certificates is needed to not have certificates error when monolith is retrieving a website's resources.

It does not happend with other backup format because the retrieving is done with a headless chrome and thus has its own certificate store
2024-12-09 20:06:06 +01:00
Daniel
346f41a12c Merge pull request #911 from AmadeusGraves/dev
Update Spanish Lang
2024-12-08 07:42:09 -05:00
Daniel
2ffbede170 Merge pull request #910 from il516/rss-feed-subscriptions
feat(rss): Add RSS Subscriptions
2024-12-08 07:41:14 -05:00
daniel31x13
c148c2b953 improvements 2024-12-08 07:37:27 -05:00
daniel31x13
a872f218fb added a base prompt for the tag generation... 2024-12-05 14:04:02 -05:00
Isaac Wise
ff1f87cb35 uncomment collection table cell 2024-12-05 02:49:37 -06:00
AmadeusGraves
94b143c91a Update common.json
Upload translation to spanish.
2024-12-05 09:48:07 +01:00
Isaac Wise
52a11040f6 Fix capitalization 2024-12-05 02:22:38 -06:00
Isaac Wise
e9c43d75fe remove console log 2024-12-05 02:04:21 -06:00
Isaac Wise
47b226cf1f format 2024-12-05 02:01:47 -06:00
Isaac Wise
266f834018 RSS form 2024-12-05 01:53:53 -06:00
Isaac Wise
c9885c0b73 format 2024-12-04 16:56:26 -06:00
Isaac
ec885a7db2 Merge branch 'linkwarden:main' into rss-feed-subscriptions 2024-12-04 16:52:52 -06:00
Isaac Wise
5665fbb412 Include collection name in table 2024-12-04 16:51:41 -06:00
Isaac Wise
9e933bd630 Update yarn lock 2024-12-04 01:55:29 -06:00
Isaac Wise
3572e101fb Table & Delete modal 2024-12-04 01:53:25 -06:00
Isaac Wise
fb06549330 Delete RSS Subscription 2024-12-04 01:18:11 -06:00
Isaac Wise
991f12566f Start RSS Subscriptions 2024-12-04 01:12:05 -06:00
dereulenspiegel
160845319f Now limiting the read size for imports in a configuration compatible way to the previous implementation 2024-12-01 15:27:16 +01:00
Daniel
bff5a7ae9a Merge pull request #903 from keizie/fix/import-url-slice
fix(import): keep url not sliced
2024-12-01 06:26:56 -05:00
Daniel
6e01c135dc increase the url limits 2024-12-01 06:25:13 -05:00
Daniel
f32e5a8d05 Merge pull request #897 from 7Adrian/7Adrian-pl-language
Added Polish "polski" language.
2024-12-01 06:02:55 -05:00
Daniel
e9003e5c45 added locale to configs 2024-12-01 05:56:38 -05:00
keizie
9065756686 fix(import): save pinnedLinks from Linkwarden export json 2024-12-01 15:25:30 +09:00
keizie
881df93c02 fix(import): keep url not sliced 2024-12-01 13:01:43 +09:00
Daniel
ed7fab0473 Merge pull request #899 from linkwarden/dev
minor fix
2024-11-30 16:13:46 -05:00
daniel31x13
38d054e143 minor fix 2024-11-30 16:12:10 -05:00
Daniel
5e89658a11 Merge pull request #898 from linkwarden/dev
Dev
2024-11-30 16:10:47 -05:00
daniel31x13
d964e02ba1 update readme 2024-11-30 16:09:31 -05:00
daniel31x13
a36eb23096 update version number 2024-11-30 15:55:36 -05:00
7Adrian
8f875d15b0 Added Polish "polski" language.
Add Polish language to public/locales/pl/common.json.
2024-11-30 17:38:48 +01:00
Daniel
9f660ff70f Merge pull request #885 from Go-rom/2.8.3-french-translation
2.8.3 french translation
2024-11-29 23:32:52 -05:00
Daniel
e6f43bbbfa Merge pull request #896 from keizie/fix-monolith-error
fix(monolith): quote url
2024-11-29 23:31:10 -05:00
keizie
1609868149 fix(monolith): quote url
when url include ampersand shell fails
```
/bin/sh: 1: amp: not found
/bin/sh: 1: -j: not found
Uncaught Monolith error...
```
2024-11-30 11:07:56 +09:00
daniel31x13
9075618e00 added ai tagging to the settings 2024-11-29 12:57:42 -05:00
Katie Bohnenkamper
179cd18ac5 Sub-collections shown on collection page 2024-11-29 00:25:50 -06:00
Katie Bohnenkamper
5a5fa9ed6c Sub-collections included in link count on tree 2024-11-29 00:24:59 -06:00
zodac
5279d94b8c Adding healthcheck to docker image 2024-11-29 13:43:37 +13:00
Gorom
5c3848e833 Update french translation 2024-11-20 17:15:59 +01:00
Gorom
27d7bbabb3 Update french translation 2024-11-20 16:56:41 +01:00
dereulenspiegel
8ff1346bf1 Adopted other import functions to handle streams instead of text 2024-11-19 12:51:21 +01:00
dereulenspiegel
71119d511e Returning 200 success to avoid stalled responses 2024-11-19 12:48:13 +01:00
dereulenspiegel
b7eb8f2c2f Made the import links dropdown menu reusable to avoid code duplication 2024-11-19 12:47:03 +01:00
dereulenspiegel
3d54cc05a4 Fixed the unnecessary creation of multiple import collections 2024-11-18 18:02:02 +01:00
dereulenspiegel
8973bdd94e Made importBookmarks reusable 2024-11-18 17:54:36 +01:00
dereulenspiegel
1af37f3619 Import should now work on testdata 2024-11-18 17:39:06 +01:00
daniel31x13
5303d63e4b add option to disable preservation 2024-11-17 16:06:05 -05:00
Daniel
05a30e1ec6 Merge pull request #867 from clemenstyp/fixed-placeholder
fixed placeholder in german translation (delete collection)
2024-11-16 09:13:21 -05:00
daniel31x13
b1a55785b5 import dates as well 2024-11-16 08:58:03 -05:00
Clemens Eyhoff
24b47e9d4b fixed an other uppercase placeholder 2024-11-16 14:49:07 +01:00
Clemens Eyhoff
34d19f9dbe fixed placeholder (was upper case) 2024-11-16 12:46:44 +01:00
dereulenspiegel
3a70e138b5 Revert "Added adm-zip dependency to be able to handle zip files for importing omnivore exports"
This reverts commit 47367c44c1.
2024-11-15 12:50:04 +01:00
dereulenspiegel
47367c44c1 Added adm-zip dependency to be able to handle zip files for importing omnivore exports 2024-11-15 12:43:17 +01:00
dereulenspiegel
e1a31481ad Added basic infrastructure to be able to have omnivore imports 2024-11-15 12:39:13 +01:00
Daniel
95dddd7da0 Merge pull request #859 from linkwarden/dev
Dev
2024-11-14 15:45:17 -05:00
daniel31x13
1a949ecdc6 bug fix 2024-11-14 15:44:31 -05:00
daniel31x13
2e6f1c207c bug fixed 2024-11-14 15:43:37 -05:00
Daniel
6aa0fa9465 Merge pull request #857 from linkwarden/dev
minor UI improvement
2024-11-14 11:01:41 -05:00
daniel31x13
8677df0340 minor UI improvement 2024-11-14 11:01:02 -05:00
Daniel
125f6ac619 Merge pull request #856 from linkwarden/dev
update version
2024-11-14 08:51:33 -05:00
daniel31x13
89ecf5c529 update version 2024-11-14 08:50:52 -05:00
Daniel
fa78d6057f Merge pull request #855 from linkwarden/dev
bug fix
2024-11-14 08:50:10 -05:00
daniel31x13
cfc28be898 bug fix 2024-11-14 08:48:18 -05:00
Daniel
c8efd4f9db Merge pull request #852 from linkwarden/dev
created check-branch.yml
2024-11-14 02:43:50 -05:00
daniel31x13
ada4e53b46 created check-branch.yml 2024-11-14 02:42:01 -05:00
Daniel
91494b0188 Merge pull request #849 from linkwarden/dev
increase staticPageGenerationTimeout
2024-11-13 22:20:17 -05:00
daniel31x13
e9fd6ec4d5 increase staticPageGenerationTimeout 2024-11-13 22:19:18 -05:00
Daniel
f08f4058dc Merge pull request #846 from linkwarden/dev
update Dockerfile
2024-11-13 09:23:59 -05:00
daniel31x13
d60200205a update Dockerfile 2024-11-13 09:23:22 -05:00
Daniel
de38eb2963 Merge pull request #845 from linkwarden/dev
revert dockerfile to working state
2024-11-13 06:14:46 -05:00
daniel31x13
f22dd4535d revert dockerfile to working state 2024-11-13 06:12:39 -05:00
Daniel
043589b301 Merge pull request #841 from linkwarden/dev
update version number
2024-11-13 00:01:50 -05:00
daniel31x13
4556827d79 update version number 2024-11-13 00:01:12 -05:00
Daniel
98ebd6d7bc Merge pull request #840 from linkwarden/dev
Dev
2024-11-12 23:59:47 -05:00
daniel31x13
0a3ca4a1d4 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2024-11-12 23:58:53 -05:00
daniel31x13
106410f55a revert change 2024-11-12 23:58:50 -05:00
Daniel
1ffe1b68a9 Merge pull request #839 from linkwarden/dev
Dev
2024-11-12 23:15:52 -05:00
Daniel
91ab0e609b Merge pull request #833 from click0/Ukrainian-translation
Corrected Ukrainian translation for November 12, 2024
2024-11-12 23:13:38 -05:00
daniel31x13
cbb7a666cd bug fix 2024-11-12 23:12:20 -05:00
Daniel
e8cf14334f Merge pull request #838 from linkwarden/dev
added status badges
2024-11-12 22:50:33 -05:00
daniel31x13
019790791b added status badges 2024-11-12 22:49:38 -05:00
Daniel
e41ba2668f Merge pull request #837 from linkwarden/dev
bug fix
2024-11-12 22:18:25 -05:00
daniel31x13
66a09fdc4b bug fix 2024-11-12 22:16:42 -05:00
Daniel
e50143ca7e Merge pull request #835 from linkwarden/dev
minor change
2024-11-12 18:22:28 -05:00
daniel31x13
162b120e55 minor change 2024-11-12 18:21:58 -05:00
Daniel
b4dd47aa37 Merge pull request #834 from linkwarden/dev
updated README
2024-11-12 17:40:32 -05:00
daniel31x13
256c232a85 updated README 2024-11-12 17:39:48 -05:00
vlad11
b7ddf22662 Corrected Ukrainian translation for November 12, 2024
Signed-off-by: vlad11 <admin@support.od.ua>
2024-11-13 00:12:32 +02:00
Daniel
5f60e9833e Merge pull request #831 from linkwarden/dev
bug fix
2024-11-12 16:18:15 -05:00
daniel31x13
ceed23ff51 bug fix 2024-11-12 16:17:38 -05:00
daniel31x13
a4c83dc82f small fix 2024-11-12 08:36:40 -05:00
daniel31x13
46f81ebf25 add info to inviteModal 2024-11-10 16:42:04 -05:00
daniel31x13
0ac5009a4a minor change 2024-11-10 00:27:13 -05:00
daniel31x13
6842da4283 new feature: open all links 2024-11-09 23:59:12 -05:00
daniel31x13
78ecf3ddb5 bug fix 2024-11-09 23:33:13 -05:00
daniel31x13
e39645e135 bug fix 2024-11-09 23:11:03 -05:00
daniel31x13
836360f99d bug fixed 2024-11-09 23:07:01 -05:00
daniel31x13
9c9fd969bc minor fix 2024-11-09 15:27:15 -05:00
daniel31x13
213105942b minor change 2024-11-09 15:02:59 -05:00
daniel31x13
0b7acb35b7 minor change 2024-11-09 14:14:13 -05:00
daniel31x13
9b58ea5c98 minor change 2024-11-09 13:45:11 -05:00
daniel31x13
c85c3bb0d7 minor fix 2024-11-08 18:03:00 -05:00
daniel31x13
7ca574b76f bug fixes 2024-11-08 17:57:50 -05:00
daniel31x13
8593df4673 bug fixed 2024-11-08 17:21:20 -05:00
daniel31x13
ddc2079f4b minor fix 2024-11-08 12:25:31 -05:00
daniel31x13
0de5caffa1 minor fix 2024-11-08 05:24:18 -05:00
daniel31x13
b14e77bdf9 minor fix 2024-11-08 04:48:31 -05:00
daniel31x13
8d366ae7d8 minor fix 2024-11-07 16:54:51 -05:00
daniel31x13
a18938ba2a minor fix 2024-11-07 16:46:26 -05:00
daniel31x13
6eac8423f8 added survey 2024-11-07 11:09:36 -05:00
daniel31x13
cbf93dcf06 minor improvement 2024-11-07 07:32:06 -05:00
daniel31x13
2993347dc7 sleeker dashboard items 2024-11-07 02:09:56 -05:00
daniel31x13
cc45c8fc3e minor improvement 2024-11-07 01:17:30 -05:00
daniel31x13
d5602a09cd minor fix 2024-11-07 01:14:23 -05:00
daniel31x13
736e98ac7d improvements 2024-11-07 01:12:05 -05:00
daniel31x13
7eaff332a9 bug fixed 2024-11-07 00:20:57 -05:00
daniel31x13
7931e2d7b6 better logic when showing link icons 2024-11-07 00:19:12 -05:00
daniel31x13
ac3888f9b3 icon picker is now much more efficient 2024-11-06 23:57:20 -05:00
daniel31x13
ac8add8c5d small improvement 2024-11-06 22:53:21 -05:00
daniel31x13
a6a0f6965b bug fixed 2024-11-06 03:36:02 -05:00
Daniel
b2c5c3c6dd Merge pull request #792 from jvanbruegge/prisma-update
Update prisma to v5
2024-11-03 14:20:24 -05:00
Daniel
4555874725 Merge branch 'dev' into prisma-update 2024-11-03 14:19:28 -05:00
daniel31x13
0f5b70eda7 update prisma 2024-11-03 03:59:39 -05:00
daniel31x13
d1c3748681 minor improvement 2024-11-03 03:34:21 -05:00
Daniel
2524139113 Merge pull request #816 from linkwarden/main
Merge main to dev
2024-11-03 03:19:40 -05:00
Daniel
6c2b86fc4b Merge branch 'dev' into main 2024-11-03 03:19:29 -05:00
Daniel
d0e0526655 Merge pull request #815 from Green-Kite/dev
update german translation
2024-11-03 03:10:40 -05:00
Green-Kite
43e94ebd0b update german translation
updated german translation
2024-11-03 07:24:38 +01:00
Daniel
aeafe6e15d Merge pull request #789 from jvanbruegge/playwright-path
Allow to specify a custom playwright browser path
2024-11-02 21:59:12 -04:00
daniel31x13
5ec221d87d update .env.sample 2024-11-02 21:58:44 -04:00
Daniel
d6d6442bc4 Merge pull request #809 from Green-Kite/dev
update german translation
2024-11-02 20:57:31 -04:00
Daniel
d12d12518e Merge pull request #636 from bjoerndot/tags-in-public-collection
Tags in public collection
2024-11-02 20:55:16 -04:00
daniel31x13
02ced62832 final change 2024-11-02 20:45:31 -04:00
daniel31x13
4febe1ace5 minor changes 2024-11-02 20:43:53 -04:00
daniel31x13
2e1e94112f make tags visible on public collections 2024-11-02 18:16:38 -04:00
daniel31x13
d86bbcd940 minor fix 2024-11-02 18:07:16 -04:00
daniel31x13
eed80ca812 add migration 2024-11-02 18:01:36 -04:00
Daniel
394251c1f1 Merge branch 'dev' into tags-in-public-collection 2024-11-02 17:56:43 -04:00
Daniel
68cdde91ad Merge pull request #813 from linkwarden/feat/team-support
Feat/seats support
2024-11-02 17:52:17 -04:00
Green-Kite
1ef286a38c update german translation
German translation updated
2024-11-01 10:08:14 +01:00
daniel31x13
508844dd9d bug fixes 2024-10-30 16:47:40 -04:00
daniel31x13
fa1f9873d5 minor change 2024-10-30 13:56:07 -04:00
Oliver Schwamb
891803547e retrieve all links for collection 2024-10-30 12:10:53 +01:00
Oliver Schwamb
24d45f8e8e Merge remote-tracking branch 'upstream/dev' into tags-in-public-collection 2024-10-30 12:10:30 +01:00
daniel31x13
f95350405c rename variable 2024-10-29 18:14:35 -04:00
daniel31x13
665019dc59 finalizing team support 2024-10-29 18:08:47 -04:00
daniel31x13
b09de5a8af updated verify max link logic 2024-10-26 13:44:52 -04:00
daniel31x13
cfd33e9bd1 bug fixed 2024-10-26 10:58:27 -04:00
daniel31x13
d3d2d5069e add member onboarding 2024-10-26 09:42:21 -04:00
daniel31x13
cffc74caa4 add team invitation functionality [WIP] 2024-10-21 13:59:05 -04:00
Jan van Brügge
3cd8eadee3 Update prisma to v5 2024-10-08 16:25:36 +01:00
daniel31x13
d146ec296c bug fixed 2024-10-07 23:43:44 -04:00
Jan van Brügge
fb4aa42eef Allow to specify a custom playwright browser path 2024-10-07 15:05:48 +01:00
daniel31x13
f68582e28c bug fixed 2024-10-07 00:57:36 -04:00
daniel31x13
d042c82cb0 add subscription webhook 2024-10-06 01:59:31 -04:00
Daniel
8738dd45e9 Merge pull request #771 from click0/main
Corrected Ukrainian translation.
2024-09-19 14:13:37 -04:00
Vladyslav V. Prodan
839de18d7a Merge branch 'linkwarden:main' into main 2024-09-19 00:46:06 +03:00
vlad11
2ba0851fee Corrected Ukrainian translation.
Signed-off-by: vlad11 <admin@support.od.ua>
2024-09-19 00:36:16 +03:00
daniel31x13
d99972a335 minor fix 2024-09-18 12:10:45 -04:00
daniel31x13
e071b9eb07 minor fix 2024-09-18 11:39:31 -04:00
daniel31x13
eb00d151b7 added locale to the config file 2024-09-18 11:06:31 -04:00
Daniel
22aaa52b3e Merge pull request #770 from bennyz327/dev
feat(lang): add traditional chinese translate
2024-09-18 11:04:03 -04:00
Benny Chou
4541277b28 feat(lang): add traditional chinese translate 2024-09-18 15:21:19 +08:00
Daniel
39faece9d7 Merge pull request #769 from linkwarden/main
Merge pull request #766 from linkwarden/daniel31x13-patch-1
2024-09-17 14:08:44 -04:00
daniel31x13
a21b0760de remove unused type 2024-09-17 14:06:03 -04:00
daniel31x13
04149fe86b Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2024-09-17 14:03:07 -04:00
daniel31x13
ff6e71d494 add schema validation for PUT requests 2024-09-17 14:03:05 -04:00
Daniel
5b02c1cfc9 Merge pull request #765 from ochtum/dev
Translated the added IDs.
2024-09-15 13:53:00 -04:00
Daniel
1ff13e8aa0 Merge pull request #767 from linkwarden/daniel31x13-patch-1
Update .env.sample
2024-09-15 13:52:47 -04:00
Daniel
eaf4524598 Merge pull request #766 from linkwarden/daniel31x13-patch-1
Update .env.sample
2024-09-15 13:52:17 -04:00
Daniel
a276065288 Update .env.sample 2024-09-15 13:51:09 -04:00
daniel31x13
1cf7421b76 added zod for post requests 2024-09-14 16:00:19 -04:00
武田 淳一
ed4a334024 Translated the added IDs. 2024-09-14 13:24:29 +09:00
Daniel
a5b1952e0d Merge pull request #710 from arran4/patch-1
Please 'EXPOSE' port 3000
2024-09-13 01:57:56 -04:00
Daniel
01826b1634 Merge branch 'dev' into patch-1 2024-09-13 01:57:39 -04:00
daniel31x13
3b17d4ddfe bug fixed 2024-09-13 00:37:58 -04:00
Daniel
f104fa095f Merge pull request #672 from jlssmt/logging
disabled query logging as default
2024-09-12 23:36:01 -04:00
Daniel
b08e6690f3 Merge pull request #689 from stumpylog/chore/update-actions
Chore: Updates actions to their latest versions
2024-09-12 23:16:27 -04:00
Daniel
33a654d21a Merge pull request #688 from stumpylog/feature/docker-file-reduce
fix: reduce Docker image size
2024-09-12 23:06:08 -04:00
Daniel
e1262142f8 Merge pull request #764 from click0/main
Corrected Ukrainian translation for September 12, 2024.
2024-09-12 19:04:50 -04:00
vlad11
0a43279665 Corrected Ukrainian translation for September 12, 2024.
Signed-off-by: vlad11 <admin@support.od.ua>
2024-09-13 01:29:34 +03:00
daniel31x13
5491ac74a5 add nl and tr translations 2024-09-12 17:06:36 -04:00
Daniel
bbcfca4cde Merge pull request #716 from kgnfth/main
feat(translations): Add Dutch and Turkish translations
2024-09-12 17:05:19 -04:00
daniel31x13
bf9a7d4fa0 add german translation 2024-09-12 17:01:38 -04:00
Daniel
edf4e489ec Merge pull request #711 from Green-Kite/main
Add German Translation
2024-09-12 17:00:49 -04:00
daniel31x13
20c5a20851 add spanish translation 2024-09-12 17:00:26 -04:00
Daniel
6f47a20e87 Merge pull request #717 from joser93/es-patch-1
Patch for Spanish translation.
2024-09-12 16:56:31 -04:00
Daniel
384937e210 Merge pull request #714 from phampyk/main
Added Spanish translation
2024-09-12 16:54:58 -04:00
Daniel
d22d989c91 Merge pull request #724 from ochtum/main
Created Japanese Translate
2024-09-12 16:53:24 -04:00
Daniel
4e0294322f Merge branch 'dev' into main 2024-09-12 16:53:12 -04:00
daniel31x13
75d5061bdf minor fix 2024-09-12 16:52:26 -04:00
Daniel
0150a9a6e3 Merge pull request #762 from rdeavila/dev
Update pt-BR translation
2024-09-12 16:49:38 -04:00
Daniel
87b79ffbac Merge pull request #726 from CoffeeAnon/feat/set-max-workers
Add Configurable Playwright Concurrency via Environment Variable
2024-09-12 16:43:32 -04:00
Rodrigo de Avila
5a40677191 Update pt-BR translation 2024-09-12 17:42:17 -03:00
Daniel
95ce2f30a8 Merge pull request #734 from click0/main
Added Ukranian translation
2024-09-12 16:33:28 -04:00
Daniel
e6a0ecbab5 Merge branch 'dev' into main 2024-09-12 16:32:39 -04:00
daniel31x13
e4c9cf8a38 add locale to config 2024-09-12 16:30:20 -04:00
Daniel
eaca3d7453 Merge pull request #746 from rdeavila/main
Add Brazilian Portuguese (pt-BR) support
2024-09-12 16:18:06 -04:00
Rodrigo de Avila
fbe3642be4 Merge branch 'linkwarden:main' into main 2024-09-12 17:16:22 -03:00
daniel31x13
bc32abbb92 Merge branch 'main' into dev
merge main to dev
2024-09-12 16:10:35 -04:00
daniel31x13
38f731f313 minor change 2024-09-12 15:46:16 -04:00
daniel31x13
aaf3590542 members with edit permission can now refresh preservation as well + bug fix 2024-09-12 15:30:15 -04:00
daniel31x13
8bb6e32bfa urls are now editable 2024-09-12 15:03:14 -04:00
daniel31x13
7bd3872195 bug fixed + optimizations 2024-09-12 13:47:18 -04:00
daniel31x13
906779010e collection closing bug fixed 2024-09-12 12:46:38 -04:00
daniel31x13
b0f87e8659 bug fixed 2024-09-12 11:59:20 -04:00
daniel31x13
653b1bc396 bug fix 2024-09-11 02:29:50 -04:00
daniel31x13
9b1506a64e add pin to hover view + add number of pins to dashboard + bug fixes 2024-09-11 01:38:38 -04:00
daniel31x13
fb1869ca7a fix dashboard bug 2024-09-10 00:09:33 -04:00
daniel31x13
5e7835b4d5 minor improvement 2024-09-09 23:27:55 -04:00
daniel31x13
0a91c47f83 minor change 2024-09-09 23:07:22 -04:00
daniel31x13
dc9db05e75 fully implemented the custom slider for the number of columns to show 2024-09-09 23:05:57 -04:00
daniel31x13
e1149c2733 minor fix 2024-09-09 19:16:28 -04:00
daniel31x13
0591d7c134 remove unused import 2024-09-09 19:09:09 -04:00
daniel31x13
4602269dd8 add number of columns slider 2024-09-09 19:05:30 -04:00
daniel31x13
9ae6a22236 minor improvement 2024-09-09 12:18:45 -04:00
daniel31x13
442da02956 minor fix 2024-09-04 23:17:58 -04:00
daniel31x13
dfcc271343 bug fix 2024-09-04 23:02:19 -04:00
daniel31x13
43d50dfd1b minor change 2024-09-04 22:39:10 -04:00
daniel31x13
40bb3e6fae fix build error 2024-09-04 22:29:54 -04:00
Daniel
3e077fa247 Merge pull request #754 from linkwarden/feat/customizable-links
Feat/customizable links
2024-09-04 22:20:16 -04:00
daniel31x13
3de8872f26 upload preview functionality 2024-09-04 22:19:40 -04:00
daniel31x13
e9072bba51 minor improvement 2024-08-30 18:10:50 -04:00
daniel31x13
d20c915970 improved edit view 2024-08-30 17:29:15 -04:00
daniel31x13
1a378de267 minor improvement 2024-08-30 10:54:27 -04:00
daniel31x13
d594159c15 minor improvement 2024-08-30 10:47:29 -04:00
daniel31x13
aee10fa406 better edit view 2024-08-30 02:38:58 -04:00
daniel31x13
820d686c37 minor improvement 2024-08-29 18:26:15 -04:00
daniel31x13
4189062c4c bug fixed 2024-08-29 12:53:37 -04:00
Daniel
1461caf68a Merge pull request #748 from linkwarden/hotfix
bug fix
2024-08-29 12:49:48 -04:00
daniel31x13
e7c7fedf8b bug fix 2024-08-29 12:47:23 -04:00
daniel31x13
b7adbbc86f improvements 2024-08-28 20:48:35 -04:00
daniel31x13
975716937f minor improvement 2024-08-28 20:30:57 -04:00
daniel31x13
2d0e52f65b better looking detail modal 2024-08-28 20:22:11 -04:00
Rodrigo de Avila
e9afe0ef25 Add Brazilian Portuguese (pt-BR) support 2024-08-28 10:26:27 -03:00
José Roberto Sánchez
a38133d618 Improved translations based on comments from @jmiguelr 2024-08-27 11:32:15 -06:00
daniel31x13
6498ae794b custom preview initial commit 2024-08-26 21:04:52 -04:00
daniel31x13
0371695eb3 choose to show which detail in each views 2024-08-26 19:56:04 -04:00
daniel31x13
9ae9c7c81a refactored view dropdown 2024-08-26 18:47:10 -04:00
daniel31x13
642374c2e5 remove commented code 2024-08-26 16:22:59 -04:00
daniel31x13
f368c2aa81 less padding for list view 2024-08-26 16:11:02 -04:00
daniel31x13
fae9e95fa9 added custom icons for links 2024-08-24 15:50:29 -04:00
Daniel
03639adc22 Merge pull request #735 from IsaacWise06/issue-691
Add new collection drop down
2024-08-22 22:48:56 -04:00
Isaac Wise
9fe829771d Add new collection drop down 2024-08-22 17:09:14 -05:00
vlad11
ed7b268c2b Created Ukranian Translate.
Signed-off-by: vlad11 <admin@support.od.ua>
2024-08-22 03:07:25 +03:00
daniel31x13
bf1a6efd2e custom icons fully implemented for collections 2024-08-20 19:25:35 -04:00
daniel31x13
6df2e44213 added translation to icon picker component + other fixes and improvements 2024-08-20 18:11:20 -04:00
daniel31x13
ae2324ecd3 progressed icon picker component 2024-08-20 16:59:01 -04:00
daniel31x13
accbd4cbfa bug fixes 2024-08-19 23:53:43 -04:00
Daniel
5f4e0d4262 Merge pull request #731 from linkwarden/hotfix
bugs fixed
2024-08-19 23:37:30 -04:00
daniel31x13
c072fed99f bugs fixed 2024-08-19 23:36:28 -04:00
Daniel
b4a9f917b5 Merge pull request #728 from linkwarden/hotfix
hotfix
2024-08-19 19:30:26 -04:00
daniel31x13
078e5ba95f minor change 2024-08-19 19:30:01 -04:00
daniel31x13
495509c888 bug fix 2024-08-19 19:25:13 -04:00
daniel31x13
dc388ebba5 improved iconPicker component + other improvements 2024-08-19 18:14:09 -04:00
Dan Jacobsen
21578bac8d feat: add configurable max workers 2024-08-19 12:44:59 -07:00
武田 淳一
1062e07065 Created Japanese Translate 2024-08-20 00:50:07 +09:00
daniel31x13
2893d3caf2 minor improvement 2024-08-18 16:52:08 -04:00
Daniel
9f74f62330 Merge pull request #722 from linkwarden/dev
Dev
2024-08-18 16:41:48 -04:00
Daniel
c6e3147bb6 Merge pull request #678 from IsaacWise06/fixes
General Fixes
2024-08-18 16:40:48 -04:00
daniel31x13
1260e8c093 fixes 2024-08-18 16:39:43 -04:00
Daniel
5cb4bdced3 Merge pull request #721 from linkwarden/feat/customizable-links
small improvements
2024-08-18 14:47:55 -04:00
Daniel
03b4240b8b Merge pull request #720 from linkwarden/revert-719-feat/customizable-links
Revert "Feat/customizable links"
2024-08-18 14:47:29 -04:00
Daniel
9a3e82470a Revert "Feat/customizable links" 2024-08-18 14:46:52 -04:00
Daniel
ee2319996b Merge pull request #719 from linkwarden/feat/customizable-links
Feat/customizable links
2024-08-18 14:46:21 -04:00
daniel31x13
c979adfe69 small improvements 2024-08-18 14:45:40 -04:00
Isaac Wise
2b83522eaa Merge branch 'dev' into fixes 2024-08-18 13:21:02 -05:00
Daniel
8c738d4a99 Merge pull request #718 from linkwarden/feat/customizable-links
Feat/customizable links
2024-08-18 14:11:47 -04:00
Isaac Wise
63678b7f1e format 2024-08-18 13:06:36 -05:00
Isaac Wise
b73e845299 Fix building 2024-08-18 13:06:19 -05:00
Isaac Wise
898b126231 Fix merge conflicts 2024-08-18 13:03:09 -05:00
daniel31x13
17d1cb45e3 minor improvement 2024-08-18 13:49:33 -04:00
José Roberto Sánchez
0aad2d9e4b Change email and fixed some typos. Overall is a good translation and I hope is merged soon. :D 2024-08-18 11:07:13 -06:00
daniel31x13
c18a5f4162 added details drawer 2024-08-18 02:55:59 -04:00
Teal'c
df7814385a feat(translations): Add Dutch and Turkish translations
- Added Dutch (nl) translations.
- Added Turkish (tr) translations.

#216
2024-08-18 04:39:35 +03:00
Ana
d568f22e00 Upload Spanish translation 2024-08-18 00:42:33 +01:00
Green-Kite
6bd1c90417 Create German Translation
updated German translation
2024-08-17 09:57:57 +02:00
daniel31x13
a40026040c icon picker component 2024-08-16 23:00:37 -04:00
Arran Ubels
334ad9f3dc Please 'EXPOSE' port 3000
This is so I can setup up Synology correctly
2024-08-17 11:56:00 +10:00
Daniel
f944345745 Merge pull request #708 from linkwarden/dev
bump version
2024-08-16 13:45:28 -04:00
daniel31x13
6b647573f0 bump version 2024-08-16 13:44:53 -04:00
Daniel
d81493e021 Merge pull request #707 from linkwarden/dev
bug fix
2024-08-16 13:43:57 -04:00
daniel31x13
03f4523d57 bug fix 2024-08-16 13:42:55 -04:00
Daniel
c24e76adac Merge pull request #706 from linkwarden/dev
v2.7.0
2024-08-16 12:36:43 -04:00
daniel31x13
5d26617251 bug fixed 2024-08-16 12:35:04 -04:00
daniel31x13
0e47ad9920 bump version 2024-08-15 16:42:36 -04:00
daniel31x13
ca45076b6c minor fix 2024-08-15 15:37:47 -04:00
Daniel
3bf6dcad2f Merge pull request #692 from phillibl/main
Update [...nextauth].ts to allow existing SSO user sign
2024-08-15 13:45:07 -04:00
daniel31x13
23860b8511 minor fix 2024-08-15 11:00:29 -04:00
daniel31x13
8758976f8d minor fix 2024-08-15 10:30:44 -04:00
Daniel
550dbd2bf0 Merge pull request #704 from shichen437/dev
feat(lang): add chinese translate
2024-08-15 08:49:26 -04:00
shichen437
04d2b3c6b2 feat(lang): add chinese translate 2024-08-15 17:20:46 +08:00
Trenton Holmes
cc1c17363b Also install a single browser (Chromium) through Playwright 2024-08-14 19:48:16 -07:00
daniel31x13
7bd0e29538 small improvement 2024-08-14 20:07:06 -04:00
daniel31x13
5baf55694c minor improvement 2024-08-14 19:23:51 -04:00
daniel31x13
193a70c6e8 fix dropdown text wrapping in other languages 2024-08-14 19:13:19 -04:00
daniel31x13
5b430cf31e add french translation 2024-08-14 17:49:53 -04:00
Daniel
684609a1dd Merge pull request #654 from zarevskaya/patch-1
Add french translation
2024-08-14 17:43:15 -04:00
Daniel
ebb2016915 Merge pull request #671 from jlssmt/main
handle undefined
2024-08-14 17:40:28 -04:00
daniel31x13
c103b66694 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2024-08-14 17:26:40 -04:00
daniel31x13
863bcc3838 bug fixed 2024-08-14 17:26:38 -04:00
Daniel
66b0aacc3f Merge pull request #660 from IsaacWise06/issue-646
fix(collections): Redirect to dashboard or login for non-public collections
2024-08-14 17:04:25 -04:00
Daniel
299498ffa6 Merge pull request #703 from linkwarden/chore/react-query-implementation
Chore/react query implementation
2024-08-14 16:45:40 -04:00
daniel31x13
8031432995 bug fix 2024-08-14 16:44:07 -04:00
daniel31x13
9cc3a7206e changes and improvements 2024-08-14 15:22:28 -04:00
daniel31x13
d15d965139 added skeleton loading 2024-08-14 13:14:06 -04:00
daniel31x13
bc04ea0fe8 fixed other views alongside card view 2024-08-13 03:19:28 -04:00
daniel31x13
bd34dacf21 bugs fixed 2024-08-13 03:01:02 -04:00
daniel31x13
80f366cd7b refactored link state management + a lot of other changes... 2024-08-13 00:08:57 -04:00
phillibl
c5602dc79f Merge pull request #1 from phillibl/phillibl-SSO-user-signin
Update [...nextauth].ts to allow existing SSO user sign
2024-08-07 05:44:19 -04:00
phillibl
0158e58d90 Update [...nextauth].ts
Fixed issue where sign in would fail for existing user if DISABLE_NEW_SSO_USERS  = true
2024-08-07 05:29:10 -04:00
Trenton Holmes
602f399119 Updates actions to their latest versions 2024-08-04 15:15:33 -07:00
Trenton Holmes
012caab606 Use multi-stage building for the monolith binary 2024-08-04 15:01:54 -07:00
jlssmt
102690fc10 handle undefined 2024-08-02 09:07:13 +02:00
daniel31x13
a73e5fa6c6 add initialData to queries 2024-08-01 18:40:08 -04:00
daniel31x13
75b1ae738f remove unused code 2024-08-01 17:43:46 -04:00
daniel31x13
8563a09a07 refactor token store 2024-08-01 17:42:57 -04:00
daniel31x13
da8dc83b8f refactor tags store 2024-08-01 17:23:51 -04:00
daniel31x13
e889509697 refactor (admin/)users store 2024-08-01 16:54:19 -04:00
Daniel
237499fd03 Merge pull request #684 from linkwarden/daniel31x13-patch-1
Update README
2024-08-01 15:55:20 -04:00
Daniel
9a287d1aef Merge pull request #683 from linkwarden/daniel31x13-patch-1
Update README.md
2024-08-01 15:54:29 -04:00
Daniel
299a2331ff Update README.md 2024-08-01 15:54:00 -04:00
daniel31x13
be5400f7cb rename users hook to user 2024-07-31 14:15:50 -04:00
daniel31x13
099bc9e054 remove old code 2024-07-30 23:23:58 -04:00
daniel31x13
5c5dd967c4 refactor account store + much smoother collection listing updates 2024-07-30 23:19:29 -04:00
daniel31x13
d1ed33b532 bug fix 2024-07-30 14:59:18 -04:00
daniel31x13
05c5bdf63c refactor collections store 2024-07-30 14:57:09 -04:00
Isaac Wise
a1248fe62f Fix issue with link action dropdown 2024-07-27 20:19:24 -05:00
Isaac Wise
8f7e0b8d09 Remove type assertions 2024-07-27 20:01:51 -05:00
Isaac Wise
9d91d2064b Merge branch 'linkwarden:main' into issue-646 2024-07-27 17:57:25 -05:00
Isaac Wise
d631754b50 Keep any types for selects 2024-07-27 17:45:54 -05:00
Isaac Wise
94be3a7448 format 2024-07-27 17:41:13 -05:00
Isaac Wise
4faf389a2b Fix more types and use logical ANDs 2024-07-27 17:40:07 -05:00
Isaac Wise
ff31732ba3 remove more ternaries 2024-07-27 16:17:38 -05:00
Isaac Wise
fa051c0d4d Merge branch 'linkwarden:main' into fixes 2024-07-26 16:42:51 -05:00
Isaac Wise
02cb93065f Redact all ids when exporting data 2024-07-26 16:41:19 -05:00
Daniel
15a0084fb7 Merge pull request #677 from linkwarden/dev
bump version
2024-07-26 12:01:38 -04:00
daniel31x13
cd82083e09 bump version 2024-07-26 12:00:46 -04:00
Daniel
c0abf2f411 Merge pull request #676 from linkwarden/dev
bug fixed
2024-07-26 11:55:07 -04:00
daniel31x13
061e22d225 bug fixed 2024-07-26 11:54:13 -04:00
Isaac Wise
0b8a9b4310 Fix some any types 2024-07-25 18:58:52 -05:00
jlssmt
ce1aa5a0ec disabled query logging as default 2024-07-25 23:19:33 +02:00
Isaac Wise
e79b98d3b0 Replace useless ternarys with logical ANDs 2024-07-22 22:34:36 -05:00
Isaac Wise
7d43ed52a4 format 2024-07-22 17:50:24 -05:00
Isaac Wise
614653bf29 Merge branch 'linkwarden:main' into issue-646 2024-07-22 17:41:34 -05:00
Isaac Wise
1b9dafbe47 Handle 400 error code when accesing a non public collection 2024-07-22 17:39:38 -05:00
zarev
abc93f1bf9 Update common.json
Correction
2024-07-20 09:55:51 +02:00
zarev
c23964a46d Create common.json
In french, if you want it ;)
2024-07-19 22:37:46 +02:00
Oliver Schwamb
abb73f80bd Reworked access to tags as public viewer 2024-07-10 10:22:58 +02:00
Oliver Schwamb
e8d0cce58a Added allLinksOfCollection to linksStore
Removed duplicated tags
Fixed overflow for line
added disclosure for tags in public collection
2024-07-09 13:50:08 +02:00
Oliver Schwamb
e045c18b7d Only show tags within collection 2024-07-05 10:50:32 +02:00
Oliver Schwamb
a1f48bbd79 Tags in public collection 2024-07-05 10:40:40 +02:00
700 changed files with 63962 additions and 20695 deletions

View File

@@ -5,3 +5,11 @@ pgdata
docker-compose.yml
Dockerfile
README.md
.yarn/install-state.gz
./apps/mobile
**/.next/cache
**/.next/cache/**
data
data.ms
.git
meili_data

View File

@@ -1,11 +1,12 @@
NEXTAUTH_SECRET=very_sensitive_secret
NEXTAUTH_URL=http://localhost:3000/api/v1/auth
NEXTAUTH_SECRET=
# Manual installation database settings
DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden
# Example: DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden
DATABASE_URL=
# Docker installation database settings
POSTGRES_PASSWORD=super_secret_password
POSTGRES_PASSWORD=
# Additional Optional Settings
PAGINATION_TAKE_COUNT=
@@ -14,7 +15,6 @@ AUTOSCROLL_TIMEOUT=
NEXT_PUBLIC_DISABLE_REGISTRATION=
NEXT_PUBLIC_CREDENTIALS_ENABLED=
DISABLE_NEW_SSO_USERS=
RE_ARCHIVE_LIMIT=
MAX_LINKS_PER_USER=
ARCHIVE_TAKE_COUNT=
BROWSER_TIMEOUT=
@@ -26,13 +26,55 @@ NEXT_PUBLIC_DEMO_USERNAME=
NEXT_PUBLIC_DEMO_PASSWORD=
NEXT_PUBLIC_ADMIN=
NEXT_PUBLIC_MAX_FILE_BUFFER=
MONOLITH_MAX_BUFFER=
MONOLITH_CUSTOM_OPTIONS=
PDF_MAX_BUFFER=
SCREENSHOT_MAX_BUFFER=
READABILITY_MAX_BUFFER=
PREVIEW_MAX_BUFFER=
MONOLITH_MAX_BUFFER=
MONOLITH_CUSTOM_OPTIONS=
IMPORT_LIMIT=
PLAYWRIGHT_LAUNCH_OPTIONS_EXECUTABLE_PATH=
PLAYWRIGHT_WS_URL=
MAX_WORKERS=
DISABLE_PRESERVATION=
NEXT_PUBLIC_RSS_POLLING_INTERVAL_MINUTES=
RSS_SUBSCRIPTION_LIMIT_PER_USER=
TEXT_CONTENT_LIMIT=
SEARCH_FILTER_LIMIT=
INDEX_TAKE_COUNT=
MEILI_TIMEOUT=
# AI Settings
NEXT_PUBLIC_OLLAMA_ENDPOINT_URL=
OLLAMA_MODEL=
# https://ai-sdk.dev/providers/openai-compatible-providers
OPENAI_API_KEY=
OPENAI_MODEL=
# Optional: Set a custom OpenAI base URL and name (for third-party providers)
CUSTOM_OPENAI_BASE_URL=
CUSTOM_OPENAI_NAME=
# https://sdk.vercel.ai/providers/ai-sdk-providers/azure
AZURE_API_KEY=
AZURE_RESOURCE_NAME=
AZURE_MODEL=
# https://sdk.vercel.ai/providers/ai-sdk-providers/anthropic
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=
# https://github.com/OpenRouterTeam/ai-sdk-provider
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=
# AWS S3 Settings
SPACES_KEY=
@@ -89,10 +131,10 @@ AUTH0_CLIENT_SECRET=
AUTH0_CLIENT_ID=
# Authelia
NEXT_PUBLIC_AUTHELIA_ENABLED=""
AUTHELIA_CLIENT_ID=""
AUTHELIA_CLIENT_SECRET=""
AUTHELIA_WELLKNOWN_URL=""
NEXT_PUBLIC_AUTHELIA_ENABLED=
AUTHELIA_CLIENT_ID=
AUTHELIA_CLIENT_SECRET=
AUTHELIA_WELLKNOWN_URL=
# Authentik
NEXT_PUBLIC_AUTHENTIK_ENABLED=
@@ -216,6 +258,7 @@ NEXT_PUBLIC_GITLAB_ENABLED=
GITLAB_CUSTOM_NAME=
GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=
GITLAB_AUTH_URL=
# Google
NEXT_PUBLIC_GOOGLE_ENABLED=
@@ -360,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=

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

View File

@@ -0,0 +1,13 @@
name: Installation Problem
title: Installation Problem
description: Report an issue with installation
labels: installation
body:
- type: textarea
id: feature-description
validations:
required: true
attributes:
label: For installation issues, please visit discord.linkwarden.app
description: "Invite link: https://discord.com/invite/CtuYV47nuJ"
placeholder: Please do not submit installation issues on GitHub.

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

18
.github/workflows/check-branch.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Check pull request source branch
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- edited
jobs:
check-branches:
runs-on: ubuntu-latest
steps:
- name: Check branches
run: |
if [ ${{ github.head_ref }} != "dev" ] && [ ${{ github.base_ref }} == "main" ]; then
echo "Merge requests to main branch are only allowed from dev branch. Please rebase your changes to dev branch."
exit 1
fi

42
.github/workflows/locale-action.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Manage i18n pull requests
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
permissions:
contents: write
pull-requests: write
jobs:
rewrite-author:
if: github.event.pull_request.head.ref == 'i18n'
runs-on: ubuntu-latest
steps:
- name: Checkout i18n branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
- name: Skip if already rewritten
run: |
if [ "$(git show -s --format='%an')" = 'LinkwardenBot' ]; then
echo "Already rewritten skipping."
exit 0
fi
- name: Configure bot identity
run: |
git config user.name "LinkwardenBot"
git config user.email "bot@linkwarden.app"
- name: Amend just the PR commits
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
git rebase --committer-date-is-author-date --exec 'git commit --amend --no-edit --allow-empty --author="LinkwardenBot <bot@linkwarden.app>"' "$BASE_SHA"
- name: Push rewritten history
run: git push --force-with-lease origin HEAD:i18n

View File

@@ -59,14 +59,20 @@ jobs:
--health-retries 5
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
- name: Use Node.js and Enable Yarn 4
uses: actions/setup-node@v4
with:
node-version: "18"
cache: 'yarn'
node-version: "20"
- name: Enable Yarn 4
run: |
sudo rm -f /usr/bin/yarn /usr/bin/yarnpkg || true
corepack enable
corepack prepare yarn@4.12.0 --activate
yarn --version
- name: Initialize PostgreSQL
run: |
echo "Initializing Databases"
@@ -74,7 +80,7 @@ jobs:
psql -h localhost -U postgres -d postgres -c "CREATE DATABASE ${{ env.TEST_POSTGRES_DATABASE }} OWNER ${{ env.TEST_POSTGRES_USER }};"
- name: Install packages
run: yarn install -y
run: yarn install --immutable
- name: Cache playwright dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
@@ -119,23 +125,19 @@ jobs:
restore-keys: |
${{ runner.os }}-playwright-
- name: Install playwright
if: steps.cache-playwright.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps
- name: Setup project
run: |
yarn prisma generate
yarn build
yarn prisma migrate deploy
yarn prisma:generate
yarn web:build
yarn prisma:deploy
- name: Start linkwarden server and worker
run: yarn start &
run: yarn concurrently:start &
- name: Run Tests
run: npx playwright test --grep ${{ matrix.test_case }}
run: yarn workspace @linkwarden/web playwright test --grep ${{ matrix.test_case }}
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report

View File

@@ -1,6 +1,7 @@
name: Create and publish a container image on release
on:
workflow_dispatch:
push:
tags:
- "*"
@@ -27,7 +28,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -40,7 +41,7 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v6
with:
context: .
push: true

37
.gitignore vendored
View File

@@ -1,13 +1,14 @@
# dependencies
/node_modules
node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
.next
/out/
# production
@@ -34,23 +35,25 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# generated files and folders
/data
.idea
prisma/dev.db
# tests
/tests
/test-results/
/blob-report/
/playwright-report/
/playwright/.cache/
/playwright/.auth/
/apps/web/tests
/apps/web/test-results/
/apps/web/blob-report/
/apps/web/playwright-report/
/apps/web/playwright/.cache/
/apps/web/playwright/.auth/
# docker
pgdata
certificates
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# generated files and folders
/data
/data.ms
meilisearch
meili_data
.idea
prisma/dev.db
.turbo
service-account-file.json

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -1,45 +0,0 @@
# Architecture
This is a summary of the architecture of Linkwarden. It's intended as a primer for collaborators to get a high-level understanding of the project.
When you start Linkwarden, there are mainly two components that run:
- The NextJS app, This is the main app and it's responsible for serving the frontend and handling the API routes.
- [The Background Worker](https://github.com/linkwarden/linkwarden/blob/main/scripts/worker.ts), This is a separate `ts-node` process that runs in the background and is responsible for archiving links.
## Main Tech Stack
- [NextJS](https://github.com/vercel/next.js)
- [TypeScript](https://github.com/microsoft/TypeScript)
- [Tailwind](https://github.com/tailwindlabs/tailwindcss)
- [DaisyUI](https://github.com/saadeghi/daisyui)
- [Prisma](https://github.com/prisma/prisma)
- [Playwright](https://github.com/microsoft/playwright)
- [Zustand](https://github.com/pmndrs/zustand)
## Folder Structure
Here's a summary of the main files and folders in the project:
```
linkwarden
├── components # React components
├── hooks # React reusable hooks
├── layouts # Layouts for pages
├── lib
│   ├── api # Server-side functions (controllers, etc.)
│   ├── client # Client-side functions
│   └── shared # Shared functions between client and server
├── pages # Pages and API routes
├── prisma # Prisma schema and migrations
├── scripts
│   ├── migration # Scripts for breaking changes
│   └── worker.ts # Background worker for archiving links
├── store # Zustand stores
├── styles # Styles
└── types # TypeScript types
```
## Versioning
We use semantic versioning for the project. You can track the changes from the [Releases](https://github.com/linkwarden/linkwarden/releases).

View File

@@ -1,4 +1,22 @@
FROM node:18.18-bullseye-slim
# Stage: monolith-builder
# Purpose: Uses the Rust image to build monolith
# Notes:
# - Fine to leave extra here, as only the resulting binary is copied out
FROM docker.io/rust:1.86-bullseye AS monolith-builder
RUN set -eux && cargo install --locked monolith
# Stage: main-app
# Purpose: Compiles the frontend and
# Notes:
# - Nothing extra should be left here. All commands should cleanup
FROM node:20.19.6-bullseye-slim AS main-app
ENV YARN_HTTP_TIMEOUT=10000000
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
ENV PRISMA_HIDE_UPDATE_MESSAGE=1
ARG DEBIAN_FRONTEND=noninteractive
@@ -6,35 +24,47 @@ RUN mkdir /data
WORKDIR /data
COPY ./package.json ./yarn.lock ./playwright.config.ts ./
RUN corepack enable
RUN --mount=type=cache,sharing=locked,target=/usr/local/share/.cache/yarn yarn install --network-timeout 10000000
COPY ./.yarnrc.yml ./
RUN apt-get update
COPY ./apps/web/package.json ./apps/web/playwright.config.ts ./apps/web/
RUN apt-get install -y \
build-essential \
curl \
libssl-dev \
pkg-config
COPY ./apps/worker/package.json ./apps/worker/
RUN apt-get update
COPY ./packages ./packages
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
COPY ./yarn.lock ./package.json ./
ENV PATH="/root/.cargo/bin:${PATH}"
RUN --mount=type=cache,sharing=locked,target=/usr/local/share/.cache/yarn \
set -eux && \
yarn workspaces focus linkwarden @linkwarden/web @linkwarden/worker && \
# Install curl for healthcheck, and ca-certificates to prevent monolith from failing to retrieve resources due to invalid certificates
apt-get update && \
apt-get install -yqq --no-install-recommends curl ca-certificates && \
apt-get autoremove && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN cargo install monolith
# Copy the compiled monolith binary from the builder stage
COPY --from=monolith-builder /usr/local/cargo/bin/monolith /usr/local/bin/monolith
RUN npx playwright install-deps && \
RUN set -eux && \
apt-get clean && \
yarn cache clean
RUN yarn playwright install
COPY . .
RUN yarn prisma generate && \
yarn build
RUN yarn prisma:generate && \
yarn web:build && \
rm -rf apps/web/.next/cache
CMD yarn prisma migrate deploy && yarn start
HEALTHCHECK --interval=30s \
--timeout=5s \
--start-period=10s \
--retries=3 \
CMD [ "/usr/bin/curl", "--silent", "--fail", "http://127.0.0.1:3000/" ]
EXPOSE 3000
CMD ["sh", "-c", "yarn prisma:deploy && yarn concurrently:start"]

138
README.md
View File

@@ -1,80 +1,86 @@
<div align="center">
<img src="./assets/logo.png" width="100px" />
<h1>Linkwarden</h1>
<h3>Bookmarks, Evolved</h3>
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat" alt="Discord"></a>
<a href="https://twitter.com/LinkwardenHQ"><img src="https://img.shields.io/twitter/follow/linkwarden" alt="Twitter"></a>
<a href="https://twitter.com/LinkwardenHQ"><img src="https://img.shields.io/twitter/follow/linkwarden" alt="Twitter"></a> <a href="https://news.ycombinator.com/item?id=36942308"><img src="https://img.shields.io/badge/Hacker%20News-280-%23FF6600"></img></a>
<img alt="GitHub commits since latest release" src="https://img.shields.io/github/commits-since/linkwarden/linkwarden/latest/dev?style=for-the-badge&label=COMMITS%20SINCE%20LATEST%20RELEASE">
<a href="https://github.com/linkwarden/linkwarden/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/linkwarden/linkwarden"></a>
<a href="https://crowdin.com/project/linkwarden">
<img src="https://badges.crowdin.net/linkwarden/localized.svg" alt="Crowdin" /></a>
<a href="https://opencollective.com/linkwarden"><img src="https://img.shields.io/opencollective/all/linkwarden" alt="Open Collective"></a>
</div>
<div align='center'>
[Website](https://linkwarden.app) | [Getting Started](https://docs.linkwarden.app) | [Features](https://github.com/linkwarden/linkwarden#features) | [Roadmap](https://github.com/orgs/linkwarden/projects/1) | [Support ❤](https://github.com/linkwarden/linkwarden#support-)
[« LAUNCH DEMO »](https://demo.linkwarden.app)
[Cloud](https://cloud.linkwarden.app) · [Website](https://linkwarden.app) · [Features](https://github.com/linkwarden/linkwarden#features) · [Docs](https://docs.linkwarden.app)
<img src="./assets/home.png" />
</div>
## Intro & motivation
**Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.**
**Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, read, annotate, and fully preserve what matters, all in one place.**
The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://en.wikipedia.org/wiki/Link_rot)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
Additionally, Linkwarden is designed with collaboration in mind, sharing links with the public and/or allowing multiple users to work together seamlessly.
In addition to preservation, Linkwarden provides a user-friendly reading and annotation experience that blends the simplicity of a “read-it-later” tool with the reliability of a web archive. Whether youre highlighting key ideas, jotting down thoughts, or revisiting content long after its disappeared from the web, Linkwarden keeps your knowledge accessible and organized.
Linkwarden is also designed with collaboration in mind, enabling you to share links with the public and/or collaborate seamlessly with multiple users.
> [!TIP]
> Our official [Cloud](https://linkwarden.app/#pricing) offering provides the simplest way to begin using Linkwarden and it's the preferred choice for many due to its time-saving benefits. <br> Your subscription supports our hosting infrastructure and ongoing development. <br> Alternatively, if you prefer [self-hosting](https://docs.linkwarden.app/self-hosting/installation) Linkwarden, no problem! You'll still have access to all the premium features.
<img src="./assets/dashboard.png" />
<div align="center">
<img src="./assets/all_links.jpg" width="23%" />
<img src="./assets/list_view.jpg" width="23%" />
<img src="./assets/all_collections.jpg" width="23%" />
<img src="./assets/manage_team.jpg" width="23%" />
<img src="./assets/readable_view.jpg" width="23%" />
<img src="./assets/preserved_formats.jpg" width="23%" />
<img src="./assets/public_page.jpg" width="23%" />
<img src="./assets/light_dashboard.jpg" width="23%" />
</div>
<details>
<summary><b>A bit of a "history"</b></summary>
Linkwarden has been completely rebuilt and redesigned from ground up, so pretty much the only thing it has in common with its predecessor is the idea behind it - bookmark management.
**What happened to the old version?**
We've forked the old version from the current repository into [this repo](https://github.com/linkwarden/linkwarden-old).
</details>
> Our official [Cloud](https://linkwarden.app/#pricing) offering provides the simplest way to begin using Linkwarden and it's the preferred choice for many due to its time-saving benefits. <br> Your subscription supports our hosting infrastructure and ongoing development. <br> Alternatively, if you prefer self-hosting Linkwarden, you can do so by following our [Installation documentation](https://docs.linkwarden.app/self-hosting/installation).
## Features
- 📸 Auto capture a screenshot, PDF, single html file, and readable view of each webpage.
- 🏛️ Send your webpage to Wayback Machine ([archive.org](https://archive.org)) for a snapshot. (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, managed by the community. [Star it here!](https://github.com/linkwarden/browser-extension)
- ⬇️ 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.
- ✨ And so many more 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
- 📱 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)!)
- ⬆️ 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 ⭐
@@ -92,25 +98,39 @@ Join and follow us in the following platforms to stay up to date about the most
## Suggestions
We _usually_ go after the [popular suggestions](https://github.com/linkwarden/linkwarden/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc). Feel free to open a [new issue](https://github.com/linkwarden/linkwarden/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.md&title=) to suggest one - others might be interested too! :)
We _usually_ go after the [popular suggestions](https://github.com/linkwarden/linkwarden/issues?q=is%3Aissue%20is%3Aopen%20sort%3Areactions-%2B1-desc). Feel free to open a [new issue](https://github.com/linkwarden/linkwarden/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.md&title=) to suggest one - others might be interested too! :)
## Roadmap
Make sure to check out our [public roadmap](https://github.com/orgs/linkwarden/projects/1).
## Docs
## Community Projects
For information on how to get started or to set up your own instance, please visit the [documentation](https://docs.linkwarden.app).
Here are some community-maintained projects that are built around Linkwarden:
- [My Links](https://apps.apple.com/ca/app/my-links-for-linkwarden/id6504573402) - iOS and MacOS Apps, maintained by [JGeek00](https://github.com/JGeek00).
- [LinkDroid](https://fossdroid.com/a/linkdroid-for-linkwarden.html) - Android App with share sheet integration, [source code](https://github.com/Dacid99/LinkDroid-for-Linkwarden).
- [LinkGuardian](https://github.com/Elbullazul/LinkGuardian) - An Android client for Linkwarden. Built with Kotlin and Jetpack compose.
- [StarWarden](https://github.com/rtuszik/starwarden) - A browser extension to save your starred GitHub repositories to Linkwarden.
## Development
If you want to contribute, Thanks! Start by checking our [public roadmap](https://github.com/orgs/linkwarden/projects/1), there you'll see a [README for contributers](https://github.com/orgs/linkwarden/projects/1?pane=issue&itemId=34708277) for the rest of the info on how to contribute and the main tech stack.
If you want to contribute, Thanks! Start by choosing one of our [popular suggestions](https://github.com/linkwarden/linkwarden/issues?q=is%3Aissue%20is%3Aopen%20sort%3Areactions-%2B1-desc), just please stay in touch with [@daniel31x13](https://github.com/daniel31x13) before starting.
# Translations
If you want to help us translate Linkwarden to your language, please check out our [Crowdin page](https://crowdin.com/project/linkwarden) and start translating. We would love to have your help!
To start translating a new language, please create an issue so we can set it up for you. New languages will be added once they reach at least 50% translation completion.
<a href="https://crowdin.com/project/linkwarden">
<img src="https://badges.crowdin.net/linkwarden/localized.svg" alt="Crowdin" /></a>
## Security
If you found a security vulnerability, please do **not** create a public issue, instead send an email to [security@linkwarden.app](mailto:security@linkwarden.app) stating the vulnerability. Thanks!
## Support
## Support <3
Other than using our official [Cloud](https://linkwarden.app/#pricing) offering, any [donations](https://opencollective.com/linkwarden) are highly appreciated as well!

42
apps/mobile/.easignore Normal file
View File

@@ -0,0 +1,42 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
app-example
.env
ios/
android/

2
apps/mobile/.env.sample Normal file
View File

@@ -0,0 +1,2 @@
LINKWARDEN_URL=
EXPO_PUBLIC_SHOW_LOGS=

46
apps/mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
app-example
.env
ios/
android/
service-account-file.json
.env.local

106
apps/mobile/app.json Normal file
View File

@@ -0,0 +1,106 @@
{
"expo": {
"name": "Linkwarden",
"slug": "linkwarden",
"version": "1.0.1",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "linkwarden",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true,
"bundleIdentifier": "app.linkwarden",
"entitlements": {
"com.apple.security.application-groups": ["group.app.linkwarden"]
},
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false,
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
}
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/maskable_logo.jpeg",
"backgroundColor": "#ffffff"
},
"package": "app.linkwarden"
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#FFFFFF",
"dark": {
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#171717"
}
}
],
"expo-secure-store",
[
"expo-share-intent",
{
"iosAppGroupIdentifier": "group.app.linkwarden"
}
],
[
"expo-build-properties",
{
"android": {
"enableProguardInReleaseBuilds": true,
"extraProguardRules": "-keep public class com.horcrux.svg.** {*;}",
"allowBackup": false,
"compileSdkVersion": 35,
"targetSdkVersion": 35,
"buildToolsVersion": "35.0.0",
"usesCleartextTraffic": true
}
}
],
[
"react-native-edge-to-edge",
{
"android": {
"parentTheme": "Default",
"enforceNavigationBarContrast": false
}
}
],
"./plugins/with-daynight-transparent-nav"
],
"experiments": {
"typedRoutes": true
},
"androidStatusBar": {
"backgroundColor": "#ffffff",
"barStyle": "dark-content",
"translucent": false
},
"androidNavigationBar": {
"backgroundColor": "#ffffff",
"barStyle": "dark-content"
},
"extra": {
"router": {
"origin": false
},
"eas": {
"projectId": "34f82639-7a25-4ebe-81c8-2db521b612cf"
}
},
"owner": "linkwarden"
}
}

View File

@@ -0,0 +1,81 @@
import { Tabs } from "expo-router";
import React from "react";
import { Platform } from "react-native";
import HapticTab from "@/components/HapticTab";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { Folder, Hash, House, Link, Settings } from "lucide-react-native";
export default function TabLayout() {
const { colorScheme } = useColorScheme();
return (
<Tabs
screenOptions={{
tabBarBackground: TabBarBackground,
tabBarActiveTintColor: rawTheme[colorScheme as ThemeName].primary,
tabBarInactiveTintColor: rawTheme[colorScheme as ThemeName].neutral,
tabBarButton: HapticTab,
tabBarStyle: Platform.select({
ios: {
position: "absolute",
borderTopWidth: 0,
elevation: 0,
backgroundColor: rawTheme[colorScheme as ThemeName]["base-200"],
paddingLeft: 5,
paddingRight: 5,
},
default: {
borderTopWidth: 0,
backgroundColor: rawTheme[colorScheme as ThemeName]["base-200"],
elevation: 0,
paddingLeft: 5,
paddingRight: 5,
},
}),
}}
>
<Tabs.Screen
name="dashboard"
options={{
title: "Dashboard",
headerShown: false,
tabBarIcon: ({ color }) => <House size={24} color={color} />,
}}
/>
<Tabs.Screen
name="links"
options={{
title: "Links",
headerShown: false,
tabBarIcon: ({ color }) => <Link size={24} color={color} />,
}}
/>
<Tabs.Screen
name="collections"
options={{
title: "Collections",
headerShown: false,
tabBarIcon: ({ color }) => <Folder size={24} color={color} />,
}}
/>
<Tabs.Screen
name="tags"
options={{
title: "Tags",
headerShown: false,
tabBarIcon: ({ color }) => <Hash size={24} color={color} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
headerShown: false,
tabBarIcon: ({ color }) => <Settings size={24} color={color} />,
}}
/>
</Tabs>
);
}

View File

@@ -0,0 +1,62 @@
import { useLinks } from "@linkwarden/router/links";
import { View, StyleSheet, Platform } from "react-native";
import useAuthStore from "@/store/auth";
import { useLocalSearchParams, useNavigation } from "expo-router";
import React, { useEffect } from "react";
import { useCollections } from "@linkwarden/router/collections";
import Links from "@/components/Links";
export default function LinksScreen() {
const { auth } = useAuthStore();
const { search, id } = useLocalSearchParams<{
search?: string;
id: string;
}>();
const { links, data } = useLinks(
{
sort: 0,
searchQueryString: decodeURIComponent(search ?? ""),
collectionId: Number(id),
},
auth
);
const collections = useCollections(auth);
const navigation = useNavigation();
useEffect(() => {
const activeCollection = collections.data?.filter(
(e) => e.id === Number(id)
)[0];
if (activeCollection?.name)
navigation?.setOptions?.({
headerTitle: activeCollection?.name,
headerSearchBarOptions: {
placeholder: `Search ${activeCollection.name}`,
},
});
}, [navigation]);
return (
<View
style={styles.container}
className="h-full bg-base-100"
collapsable={false}
collapsableChildren={false}
>
<Links links={links} data={data} />
</View>
);
}
const styles = StyleSheet.create({
container: Platform.select({
ios: {
paddingBottom: 83,
},
default: {},
}),
});

View File

@@ -0,0 +1,44 @@
import { Stack, useRouter } from "expo-router";
import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { Platform } from "react-native";
export default function Layout() {
const router = useRouter();
const { colorScheme } = useColorScheme();
return (
<Stack
screenOptions={{
headerTitle: "Collections",
headerLargeTitle: true,
headerTransparent: Platform.OS === "ios" ? true : false,
headerShadowVisible: false,
headerBlurEffect:
colorScheme === "dark" ? "systemMaterialDark" : "systemMaterial",
headerTintColor: colorScheme === "dark" ? "white" : "black",
headerSearchBarOptions: {
placeholder: "Search Collections",
autoCapitalize: "none",
onChangeText: (e) => {
router.setParams({
search: encodeURIComponent(e.nativeEvent.text),
});
},
headerIconColor: colorScheme === "dark" ? "white" : "black",
},
headerLargeStyle: {
backgroundColor: rawTheme[colorScheme as ThemeName]["base-100"],
},
headerStyle: {
backgroundColor:
Platform.OS === "ios"
? "transparent"
: colorScheme === "dark"
? rawTheme["dark"]["base-100"]
: "white",
},
}}
/>
);
}

View File

@@ -0,0 +1,94 @@
import {
View,
StyleSheet,
FlatList,
Platform,
Text,
ActivityIndicator,
} from "react-native";
import useAuthStore from "@/store/auth";
import CollectionListing from "@/components/CollectionListing";
import { useLocalSearchParams } from "expo-router";
import React, { useEffect, useState } from "react";
import Spinner from "@/components/ui/Spinner";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { useCollections } from "@linkwarden/router/collections";
import { CollectionIncludingMembersAndLinkCount } from "@linkwarden/types";
export default function CollectionsScreen() {
const { colorScheme } = useColorScheme();
const { auth } = useAuthStore();
const { search } = useLocalSearchParams<{ search?: string }>();
const collections = useCollections(auth);
const [filteredCollections, setFilteredCollections] = useState<
CollectionIncludingMembersAndLinkCount[]
>([]);
useEffect(() => {
const filter =
collections.data?.filter((e) =>
e.name.includes(decodeURIComponent(search || ""))
) || [];
setFilteredCollections(filter);
}, [search, collections.data]);
return (
<View
style={styles.container}
className="h-full bg-base-100"
collapsable={false}
collapsableChildren={false}
>
{collections.isLoading ? (
<View className="flex justify-center h-screen items-center">
<ActivityIndicator size="large" />
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
</View>
) : (
<FlatList
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={() => <></>}
data={filteredCollections}
refreshControl={
<Spinner
refreshing={collections.isRefetching}
onRefresh={() => collections.refetch()}
progressBackgroundColor={
rawTheme[colorScheme as ThemeName]["base-200"]
}
colors={[rawTheme[colorScheme as ThemeName]["base-content"]]}
/>
}
refreshing={collections.isRefetching}
initialNumToRender={4}
keyExtractor={(item) => item.id?.toString() || ""}
renderItem={({ item }) => <CollectionListing collection={item} />}
onEndReachedThreshold={0.5}
ItemSeparatorComponent={() => (
<View className="bg-neutral-content h-px" />
)}
ListEmptyComponent={
<View className="flex justify-center py-10 items-center">
<Text className="text-center text-xl text-neutral">
Nothing found...
</Text>
</View>
}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: Platform.select({
ios: {
paddingBottom: 83,
},
default: {},
}),
});

View File

@@ -0,0 +1,72 @@
import { useLinks } from "@linkwarden/router/links";
import { View, StyleSheet, Platform } from "react-native";
import useAuthStore from "@/store/auth";
import { useLocalSearchParams, useNavigation } from "expo-router";
import React, { useEffect, useMemo } from "react";
import { useCollections } from "@linkwarden/router/collections";
import Links from "@/components/Links";
export default function LinksScreen() {
const { auth } = useAuthStore();
const { search, section, collectionId } = useLocalSearchParams<{
search?: string;
section?: "pinned-links" | "recent-links" | "collection";
collectionId?: string;
}>();
const navigation = useNavigation();
const collections = useCollections(auth);
const title = useMemo(() => {
if (section === "pinned-links") return "Pinned Links";
if (section === "recent-links") return "Recent Links";
if (section === "collection") {
return (
collections.data?.find((c) => c.id?.toString() === collectionId)
?.name || "Collection"
);
}
return "Links";
}, [section, collections.data, collectionId]);
useEffect(() => {
navigation.setOptions({
headerTitle: title,
headerSearchBarOptions: {
placeholder: `Search ${title}`,
},
});
}, [title, navigation]);
const { links, data } = useLinks(
{
sort: 0,
searchQueryString: decodeURIComponent(search ?? ""),
collectionId: Number(collectionId),
pinnedOnly: section === "pinned-links",
},
auth
);
return (
<View
style={styles.container}
className="h-full bg-base-100"
collapsable={false}
collapsableChildren={false}
>
<Links links={links} data={data} />
</View>
);
}
const styles = StyleSheet.create({
container: Platform.select({
ios: {
paddingBottom: 83,
},
default: {},
}),
});

View File

@@ -0,0 +1,89 @@
import { rawTheme, ThemeName } from "@/lib/colors";
import { Stack, useRouter } from "expo-router";
import { Plus } from "lucide-react-native";
import { useColorScheme } from "nativewind";
import { Platform, TouchableOpacity } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import * as DropdownMenu from "zeego/dropdown-menu";
export default function Layout() {
const router = useRouter();
const { colorScheme } = useColorScheme();
return (
<Stack
screenOptions={{
headerLargeTitle: true,
headerTransparent: Platform.OS === "ios" ? true : false,
headerShadowVisible: false,
headerBlurEffect:
colorScheme === "dark" ? "systemMaterialDark" : "systemMaterial",
headerTintColor: colorScheme === "dark" ? "white" : "black",
headerLargeStyle: {
backgroundColor: rawTheme[colorScheme as ThemeName]["base-100"],
},
headerStyle: {
backgroundColor:
Platform.OS === "ios"
? "transparent"
: colorScheme === "dark"
? rawTheme["dark"]["base-100"]
: "white",
},
}}
>
<Stack.Screen
name="index"
options={{
headerTitle: "Dashboard",
headerRight: () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity>
<Plus
size={21}
color={rawTheme[colorScheme as ThemeName].primary}
/>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item
key="new-link"
onSelect={() => SheetManager.show("add-link-sheet")}
>
<DropdownMenu.ItemTitle>New Link</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="new-collection"
onSelect={() => SheetManager.show("new-collection-sheet")}
>
<DropdownMenu.ItemTitle>
New Collection
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
),
}}
/>
<Stack.Screen
name="[section]"
options={{
headerTitle: "Links",
headerBackTitle: "Back",
headerSearchBarOptions: {
placeholder: "Search",
autoCapitalize: "none",
headerIconColor: colorScheme === "dark" ? "white" : "black",
onChangeText: (e) => {
router.setParams({
search: encodeURIComponent(e.nativeEvent.text),
});
},
},
}}
/>
</Stack>
);
}

View File

@@ -0,0 +1,147 @@
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";
import { DashboardSection as DashboardSectionType } from "@linkwarden/prisma/client";
import { useUser } from "@linkwarden/router/user";
import { useCollections } from "@linkwarden/router/collections";
import { useTags } from "@linkwarden/router/tags";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import Spinner from "@/components/ui/Spinner";
import DashboardSection from "@/components/DashboardSection";
export default function DashboardScreen() {
const { auth } = useAuthStore();
const {
data: { links = [], numberOfPinnedLinks, collectionLinks = {} } = {
links: [],
},
...dashboardData
} = useDashboardData(auth);
const { data: user, ...userData } = useUser(auth);
const { data: collections = [], ...collectionsData } = useCollections(auth);
const { data: tags = [], ...tagsData } = useTags(auth);
const { colorScheme } = useColorScheme();
const [dashboardSections, setDashboardSections] = useState<
DashboardSectionType[]
>(user?.dashboardSections || []);
const [numberOfLinks, setNumberOfLinks] = useState(0);
useEffect(() => {
setDashboardSections(user?.dashboardSections || []);
}, [user?.dashboardSections]);
useEffect(() => {
setNumberOfLinks(
collections?.reduce?.(
(accumulator, collection) =>
accumulator + (collection._count as any).links,
0
)
);
}, [collections]);
const orderedSections = useMemo(() => {
return [...dashboardSections].sort((a, b) => {
const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
}, [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={pullRefreshing}
onRefresh={onRefresh}
progressBackgroundColor={
rawTheme[colorScheme as ThemeName]["base-200"]
}
colors={[rawTheme[colorScheme as ThemeName]["base-content"]]}
/>
}
contentContainerStyle={styles.container}
className="bg-base-100"
contentInsetAdjustmentBehavior="automatic"
>
{orderedSections.map((sectionData, i) => {
if (!collections || !collections[0]) return null;
const collection = collections.find(
(c) => c.id === sectionData.collectionId
);
return (
<DashboardSection
key={sectionData.id}
sectionData={sectionData}
collection={collection}
collectionLinks={
sectionData.collectionId
? collectionLinks[sectionData.collectionId]
: []
}
links={links}
tagsLength={tags.length}
numberOfLinks={numberOfLinks}
collectionsLength={collections.length}
numberOfPinnedLinks={numberOfPinnedLinks}
dashboardData={dashboardData}
/>
);
})}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: Platform.select({
ios: {
paddingBottom: 49,
flexDirection: "column",
gap: 15,
paddingVertical: 20,
},
default: {
flexDirection: "column",
gap: 15,
paddingVertical: 20,
},
}),
});

View File

@@ -0,0 +1,44 @@
import { Stack, useRouter } from "expo-router";
import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { Platform } from "react-native";
export default function Layout() {
const router = useRouter();
const { colorScheme } = useColorScheme();
return (
<Stack
screenOptions={{
headerTitle: "Links",
headerLargeTitle: true,
headerTransparent: Platform.OS === "ios" ? true : false,
headerShadowVisible: false,
headerBlurEffect:
colorScheme === "dark" ? "systemMaterialDark" : "systemMaterial",
headerTintColor: colorScheme === "dark" ? "white" : "black",
headerSearchBarOptions: {
placeholder: "Search Links",
autoCapitalize: "none",
onChangeText: (e) => {
router.setParams({
search: encodeURIComponent(e.nativeEvent.text),
});
},
headerIconColor: colorScheme === "dark" ? "white" : "black",
},
headerLargeStyle: {
backgroundColor: rawTheme[colorScheme as ThemeName]["base-100"],
},
headerStyle: {
backgroundColor:
Platform.OS === "ios"
? "transparent"
: colorScheme === "dark"
? rawTheme["dark"]["base-100"]
: "white",
},
}}
/>
);
}

View File

@@ -0,0 +1,39 @@
import { useLinks } from "@linkwarden/router/links";
import { View, StyleSheet, Platform } from "react-native";
import useAuthStore from "@/store/auth";
import { useLocalSearchParams } from "expo-router";
import React from "react";
import Links from "@/components/Links";
export default function LinksScreen() {
const { auth } = useAuthStore();
const { search } = useLocalSearchParams<{ search?: string }>();
const { links, data } = useLinks(
{
sort: 0,
searchQueryString: decodeURIComponent(search ?? ""),
},
auth
);
return (
<View
style={styles.container}
className="h-full bg-base-100"
collapsable={false}
collapsableChildren={false}
>
<Links links={links} data={data} />
</View>
);
}
const styles = StyleSheet.create({
container: Platform.select({
ios: {
paddingBottom: 83,
},
default: {},
}),
});

View File

@@ -0,0 +1,33 @@
import { Stack } from "expo-router";
import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { Platform } from "react-native";
export default function Layout() {
const { colorScheme } = useColorScheme();
return (
<Stack
screenOptions={{
headerTitle: "Settings",
headerLargeTitle: true,
headerTransparent: Platform.OS === "ios" ? true : false,
headerShadowVisible: false,
headerBlurEffect:
colorScheme === "dark" ? "systemMaterialDark" : "systemMaterial",
headerTintColor: colorScheme === "dark" ? "white" : "black",
headerLargeStyle: {
backgroundColor: rawTheme[colorScheme as ThemeName]["base-100"],
},
headerStyle: {
backgroundColor:
Platform.OS === "ios"
? "transparent"
: colorScheme === "dark"
? rawTheme["dark"]["base-100"]
: "white",
},
}}
/>
);
}

View File

@@ -0,0 +1,241 @@
import useAuthStore from "@/store/auth";
import { useUser } from "@linkwarden/router/user";
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Platform,
Alert,
} from "react-native";
import { nativeApplicationVersion } from "expo-application";
import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useEffect, useState } from "react";
import {
AppWindowMac,
Check,
ExternalLink,
LogOut,
Mail,
Moon,
Smartphone,
Sun,
} from "lucide-react-native";
import useDataStore from "@/store/data";
import * as Clipboard from "expo-clipboard";
export default function SettingsScreen() {
const { signOut, auth } = useAuthStore();
const { data: user } = useUser(auth);
const { colorScheme, setColorScheme } = useColorScheme();
const { data, updateData } = useDataStore();
const [override, setOverride] = useState<"light" | "dark" | "system">(
data.theme || "system"
);
useEffect(() => {
setColorScheme(override);
updateData({ theme: override });
}, [override]);
return (
<View
style={styles.container}
collapsable={false}
collapsableChildren={false}
className="bg-base-100"
>
<ScrollView
contentContainerStyle={{
padding: 20,
}}
contentContainerClassName="flex-col gap-6"
contentInsetAdjustmentBehavior="automatic"
>
<View className="bg-base-200 rounded-xl px-4 py-3">
<Text className="font-semibold text-xl text-base-content">
Your account
</Text>
<Text className="text-neutral mt-2 mb-3">
{user?.email || "@" + user?.username}
</Text>
<View className="h-px bg-neutral-content -mr-4" />
<TouchableOpacity
className="flex-row items-center mt-3"
onPress={() =>
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
{
text: "Cancel",
style: "cancel",
},
{
text: "Sign Out",
style: "destructive",
onPress: () => {
signOut();
},
},
])
}
>
<Text className="flex-1 text-base text-red-500">Sign Out</Text>
<LogOut size={18} color="red" />
</TouchableOpacity>
</View>
<View>
<Text className="mb-4 mx-4 text-neutral">Theme</Text>
<View className="bg-base-200 rounded-xl flex-col">
<TouchableOpacity
className="flex-row gap-2 items-center justify-between py-3 px-4"
onPress={() => setOverride("system")}
>
<View className="flex-row items-center gap-2">
<Smartphone
size={20}
color={rawTheme[colorScheme as ThemeName].neutral}
/>
<Text className="text-neutral">System Defaults</Text>
</View>
{override === "system" ? (
<Check
size={20}
color={rawTheme[colorScheme as ThemeName].primary}
/>
) : null}
</TouchableOpacity>
<View className="h-px bg-neutral-content ml-12" />
<TouchableOpacity
className="flex-row gap-2 items-center justify-between py-3 px-4"
onPress={() => setOverride("light")}
>
<View className="flex-row items-center gap-2">
<Sun size={20} color="orange" />
<Text className="text-orange-500 dark:text-orange-400">
Light
</Text>
</View>
{override === "light" ? (
<Check
size={20}
color={rawTheme[colorScheme as ThemeName].primary}
/>
) : null}
</TouchableOpacity>
<View className="h-px bg-neutral-content ml-12" />
<TouchableOpacity
className="flex-row gap-2 items-center justify-between py-3 px-4"
onPress={() => setOverride("dark")}
>
<View className="flex-row items-center gap-2">
<Moon size={20} color="royalblue" />
<Text className="text-blue-600 dark:text-blue-400">Dark</Text>
</View>
{override === "dark" ? (
<Check
size={20}
color={rawTheme[colorScheme as ThemeName].primary}
/>
) : null}
</TouchableOpacity>
</View>
</View>
<View>
<Text className="mb-4 mx-4 text-neutral">Preferred Browser</Text>
<View className="bg-base-200 rounded-xl flex-col">
<TouchableOpacity
className="flex-row gap-2 items-center justify-between py-3 px-4"
onPress={() =>
updateData({
preferredBrowser: "app",
})
}
>
<View className="flex-row items-center gap-2">
<AppWindowMac
size={20}
color={rawTheme[colorScheme as ThemeName].neutral}
/>
<Text className="text-base-content">In app browser</Text>
</View>
{data.preferredBrowser === "app" ? (
<Check
size={20}
color={rawTheme[colorScheme as ThemeName].primary}
/>
) : null}
</TouchableOpacity>
<View className="h-px bg-neutral-content ml-12" />
<TouchableOpacity
className="flex-row gap-2 items-center justify-between py-3 px-4"
onPress={() =>
updateData({
preferredBrowser: "system",
})
}
>
<View className="flex-row items-center gap-2">
<ExternalLink
size={20}
color={rawTheme[colorScheme as ThemeName].neutral}
/>
<Text className="text-base-content">
System default browser
</Text>
</View>
{data.preferredBrowser === "system" ? (
<Check
size={20}
color={rawTheme[colorScheme as ThemeName].primary}
/>
) : null}
</TouchableOpacity>
</View>
</View>
<View>
<Text className="mb-4 mx-4 text-neutral">Contact Us</Text>
<View className="bg-base-200 rounded-xl flex-col">
<TouchableOpacity
className="flex-row gap-2 items-center justify-between py-3 px-4"
onPress={async () => {
await Clipboard.setStringAsync("support@linkwarden.app");
Alert.alert("Copied to clipboard", "support@linkwarden.app");
}}
>
<View className="flex-row items-center gap-2">
<Mail
size={20}
color={rawTheme[colorScheme as ThemeName].neutral}
/>
<Text className="text-base-content">
support@linkwarden.app
</Text>
</View>
</TouchableOpacity>
</View>
</View>
<Text className="mx-auto text-sm text-neutral">
Linkwarden for {Platform.OS === "ios" ? "iOS" : "Android"}{" "}
{nativeApplicationVersion}
</Text>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: Platform.select({
ios: {
flex: 1,
paddingBottom: 83,
},
default: {
flex: 1,
},
}),
});

View File

@@ -0,0 +1,60 @@
import { useLinks } from "@linkwarden/router/links";
import { View, StyleSheet, Platform } from "react-native";
import useAuthStore from "@/store/auth";
import { useLocalSearchParams, useNavigation } from "expo-router";
import React, { useEffect } from "react";
import { useTags } from "@linkwarden/router/tags";
import Links from "@/components/Links";
export default function LinksScreen() {
const { auth } = useAuthStore();
const { search, id } = useLocalSearchParams<{
search?: string;
id: string;
}>();
const { links, data } = useLinks(
{
sort: 0,
searchQueryString: decodeURIComponent(search ?? ""),
tagId: Number(id),
},
auth
);
const tags = useTags(auth);
const navigation = useNavigation();
useEffect(() => {
const activeTag = tags.data?.filter((e) => e.id === Number(id))[0];
if (activeTag?.name)
navigation?.setOptions?.({
headerTitle: activeTag?.name,
headerSearchBarOptions: {
placeholder: `Search ${activeTag.name}`,
},
});
}, [navigation]);
return (
<View
style={styles.container}
className="h-full bg-base-100"
collapsable={false}
collapsableChildren={false}
>
<Links links={links} data={data} />
</View>
);
}
const styles = StyleSheet.create({
container: Platform.select({
ios: {
paddingBottom: 83,
},
default: {},
}),
});

View File

@@ -0,0 +1,44 @@
import { Stack, useRouter } from "expo-router";
import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { Platform } from "react-native";
export default function Layout() {
const router = useRouter();
const { colorScheme } = useColorScheme();
return (
<Stack
screenOptions={{
headerTitle: "Tags",
headerLargeTitle: true,
headerTransparent: Platform.OS === "ios" ? true : false,
headerShadowVisible: false,
headerBlurEffect:
colorScheme === "dark" ? "systemMaterialDark" : "systemMaterial",
headerTintColor: colorScheme === "dark" ? "white" : "black",
headerSearchBarOptions: {
placeholder: "Search Tags",
autoCapitalize: "none",
onChangeText: (e) => {
router.setParams({
search: encodeURIComponent(e.nativeEvent.text),
});
},
headerIconColor: colorScheme === "dark" ? "white" : "black",
},
headerLargeStyle: {
backgroundColor: rawTheme[colorScheme as ThemeName]["base-100"],
},
headerStyle: {
backgroundColor:
Platform.OS === "ios"
? "transparent"
: colorScheme === "dark"
? rawTheme["dark"]["base-100"]
: "white",
},
}}
/>
);
}

View File

@@ -0,0 +1,92 @@
import {
View,
StyleSheet,
FlatList,
Platform,
Text,
ActivityIndicator,
} from "react-native";
import useAuthStore from "@/store/auth";
import TagListing from "@/components/TagListing";
import { useLocalSearchParams } from "expo-router";
import React, { useEffect, useState } from "react";
import Spinner from "@/components/ui/Spinner";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { TagIncludingLinkCount } from "@linkwarden/types";
import { useTags } from "@linkwarden/router/tags";
export default function TagsScreen() {
const { colorScheme } = useColorScheme();
const { auth } = useAuthStore();
const { search } = useLocalSearchParams<{ search?: string }>();
const tags = useTags(auth);
const [filteredTags, setFilteredTags] = useState<TagIncludingLinkCount[]>([]);
useEffect(() => {
const filter =
tags.data?.filter((e) =>
e.name.includes(decodeURIComponent(search || ""))
) || [];
setFilteredTags(filter);
}, [search, tags.data]);
return (
<View
style={styles.container}
className="h-full bg-base-100"
collapsable={false}
collapsableChildren={false}
>
{tags.isLoading ? (
<View className="flex justify-center h-screen items-center">
<ActivityIndicator size="large" />
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
</View>
) : (
<FlatList
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={() => <></>}
data={filteredTags}
refreshControl={
<Spinner
refreshing={tags.isRefetching}
onRefresh={() => tags.refetch()}
progressBackgroundColor={
rawTheme[colorScheme as ThemeName]["base-200"]
}
colors={[rawTheme[colorScheme as ThemeName]["base-content"]]}
/>
}
refreshing={tags.isRefetching}
initialNumToRender={4}
keyExtractor={(item) => item.id?.toString() || ""}
renderItem={({ item }) => <TagListing tag={item} />}
onEndReachedThreshold={0.5}
ItemSeparatorComponent={() => (
<View className="bg-neutral-content h-px" />
)}
ListEmptyComponent={
<View className="flex justify-center py-10 items-center">
<Text className="text-center text-xl text-neutral">
Nothing found...
</Text>
</View>
}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: Platform.select({
ios: {
paddingBottom: 83,
},
default: {},
}),
});

View File

@@ -0,0 +1,20 @@
import { Link, Stack } from "expo-router";
import { View, StyleSheet } from "react-native";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Oops! This screen doesn't exist." }} />
<View style={styles.container}>
<Link href="/">Go to home screen</Link>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
});

325
apps/mobile/app/_layout.tsx Normal file
View File

@@ -0,0 +1,325 @@
import {
router,
Stack,
usePathname,
useRootNavigationState,
useRouter,
} from "expo-router";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { mmkvPersister } from "@/lib/queryPersister";
import { useState, useEffect } from "react";
import "../styles/global.css";
import { SheetManager, SheetProvider } from "react-native-actions-sheet";
import "@/components/ActionSheets/Sheets";
import { useColorScheme } from "nativewind";
import { lightTheme, darkTheme } from "../lib/theme";
import {
Alert,
Linking,
Platform,
Share,
TouchableOpacity,
View,
} from "react-native";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useShareIntent } from "expo-share-intent";
import useDataStore from "@/store/data";
import useAuthStore from "@/store/auth";
import { KeyboardProvider } from "react-native-keyboard-controller";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Compass, Ellipsis } from "lucide-react-native";
import { Chromium } from "@/components/ui/Icons";
import useTmpStore from "@/store/tmp";
import {
LinkIncludingShortenedCollectionAndTags,
MobileAuth,
} from "@linkwarden/types";
import { useDeleteLink, useUpdateLink } from "@linkwarden/router/links";
import { deleteLinkCache } from "@/lib/cache";
import { queryClient } from "@/lib/queryClient";
import getOriginalFormat from "@linkwarden/lib/getOriginalFormat";
import { StatusBar } from "expo-status-bar";
export default function RootLayout() {
const [isLoading, setIsLoading] = useState(true);
const { hasShareIntent, shareIntent, error, resetShareIntent } =
useShareIntent();
const { updateData, setData, data } = useDataStore();
const router = useRouter();
const pathname = usePathname();
const { auth, setAuth } = useAuthStore();
const rootNavState = useRootNavigationState();
useEffect(() => {
setAuth();
setData();
}, []);
useEffect(() => {
if (!rootNavState?.key) return;
if (hasShareIntent && shareIntent.webUrl) {
updateData({
shareIntent: {
hasShareIntent: true,
url: shareIntent.webUrl || "",
},
});
resetShareIntent();
}
const needsRewrite =
((typeof pathname === "string" && pathname.startsWith("/dataUrl=")) ||
hasShareIntent) &&
pathname !== "/incoming";
if (needsRewrite) {
router.replace("/incoming");
}
if (hasShareIntent) {
resetShareIntent();
router.replace("/incoming");
}
}, [
rootNavState?.key,
hasShareIntent,
pathname,
shareIntent?.webUrl,
data.shareIntent,
]);
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister: mmkvPersister,
maxAge: Infinity,
dehydrateOptions: {
shouldDehydrateMutation: () => true,
shouldDehydrateQuery: () => true,
},
}}
onSuccess={() => {
setIsLoading(false);
queryClient.invalidateQueries();
}}
>
<RootComponent isLoading={isLoading} auth={auth} />
</PersistQueryClientProvider>
);
}
const RootComponent = ({
isLoading,
auth,
}: {
isLoading: boolean;
auth: MobileAuth;
}) => {
const { colorScheme } = useColorScheme();
const updateLink = useUpdateLink(auth);
const deleteLink = useDeleteLink(auth);
const { tmp } = useTmpStore();
return (
<View
style={[{ flex: 1 }, colorScheme === "dark" ? darkTheme : lightTheme]}
>
<KeyboardProvider>
<SheetProvider>
<StatusBar
style={colorScheme === "dark" ? "light" : "dark"}
backgroundColor={rawTheme[colorScheme as ThemeName]["base-100"]}
/>
{!isLoading && (
<Stack
screenOptions={{
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",
headerStyle: {
backgroundColor:
colorScheme === "dark"
? rawTheme["dark"]["base-100"]
: "white",
},
headerRight: () => (
<View className="flex-row gap-5">
<TouchableOpacity
onPress={() => {
if (tmp.link) {
if (tmp.link.url) {
return Linking.openURL(tmp.link.url);
} else {
const format = getOriginalFormat(tmp.link);
return Linking.openURL(
format !== null
? auth.instance +
`/preserved/${tmp.link.id}?format=${format}`
: tmp.link.url || ""
);
}
}
}}
>
{Platform.OS === "ios" ? (
<Compass
size={21}
color={
rawTheme[colorScheme as ThemeName]["base-content"]
}
/>
) : (
<Chromium
stroke={
rawTheme[colorScheme as ThemeName]["base-content"]
}
/>
)}
</TouchableOpacity>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity>
<Ellipsis
size={21}
color={
rawTheme[colorScheme as ThemeName][
"base-content"
]
}
/>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{tmp.link?.url && (
<DropdownMenu.Item
key="share"
onSelect={async () => {
await Share.share({
...(Platform.OS === "android"
? { message: tmp.link?.url as string }
: { url: tmp.link?.url as string }),
});
}}
>
<DropdownMenu.ItemTitle>
Share
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
)}
{tmp.link && tmp.user && (
<DropdownMenu.Item
key="pin-link"
onSelect={async () => {
const isAlreadyPinned =
tmp.link?.pinnedBy && tmp.link.pinnedBy[0]
? true
: false;
await updateLink.mutateAsync({
...(tmp.link as LinkIncludingShortenedCollectionAndTags),
pinnedBy: (isAlreadyPinned
? [{ id: undefined }]
: [{ id: tmp.user?.id }]) as any,
});
}}
>
<DropdownMenu.ItemTitle>
{tmp.link.pinnedBy && tmp.link.pinnedBy[0]
? "Unpin Link"
: "Pin Link"}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
)}
{tmp.link && (
<DropdownMenu.Item
key="edit-link"
onSelect={() => {
SheetManager.show("edit-link-sheet", {
payload: {
link: tmp.link as LinkIncludingShortenedCollectionAndTags,
},
});
}}
>
<DropdownMenu.ItemTitle>
Edit Link
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
)}
{tmp.link && (
<DropdownMenu.Item
key="delete-link"
onSelect={() => {
return Alert.alert(
"Delete Link",
"Are you sure you want to delete this link? This action cannot be undone.",
[
{
text: "Cancel",
style: "cancel",
},
{
text: "Delete",
style: "destructive",
onPress: () => {
deleteLink.mutate(
tmp.link?.id as number,
{
onSuccess: async () => {
await deleteLinkCache(
tmp.link?.id as number
);
},
}
);
// go back
router.back();
},
},
]
);
}}
>
<DropdownMenu.ItemTitle>
Delete
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
)}
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
),
}}
/>
<Stack.Screen name="login" />
<Stack.Screen name="index" />
<Stack.Screen name="incoming" />
<Stack.Screen name="+not-found" />
</Stack>
)}
</SheetProvider>
</KeyboardProvider>
</View>
);
};

View File

@@ -0,0 +1,98 @@
import React, { useEffect } from "react";
import {
SafeAreaView,
View,
Text,
StyleSheet,
ActivityIndicator,
Alert,
} from "react-native";
import { Redirect, useRouter } from "expo-router";
import useAuthStore from "@/store/auth";
import useDataStore from "@/store/data";
import { Check } from "lucide-react-native";
import { useAddLink } from "@linkwarden/router/links";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
export default function IncomingScreen() {
const { auth } = useAuthStore();
const router = useRouter();
const { data, updateData } = useDataStore();
const addLink = useAddLink(auth);
const { colorScheme } = useColorScheme();
useEffect(() => {
if (auth.status === "authenticated" && data.shareIntent.url)
addLink.mutate(
{ url: data.shareIntent.url },
{
onSuccess: () => {
setTimeout(() => {
updateData({
shareIntent: {
hasShareIntent: false,
url: "",
},
});
router.replace("/dashboard");
}, 1000);
},
onError: (error) => {
Alert.alert("Error", "There was an error adding the link.");
console.error("Error adding link:", error);
},
}
);
}, [auth, data.shareIntent.url]);
if (auth.status === "unauthenticated") return <Redirect href="/" />;
return (
<SafeAreaView className="flex-1 bg-base-100">
{data?.shareIntent.url ? (
<View className="flex-1 items-center justify-center">
<Check
size={140}
className="mb-3 text-base-content"
color={rawTheme[colorScheme as ThemeName].primary}
/>
<Text className="text-2xl font-semibold text-base-content">
Link Saved!
</Text>
</View>
) : (
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" />
<Text className="mt-3 text-base text-base-content opacity-70">
One sec {String(data?.shareIntent.url)}
</Text>
</View>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
center: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
check: {
marginBottom: 12,
},
title: {
fontSize: 28,
fontWeight: "600",
},
subtitle: {
marginTop: 12,
fontSize: 16,
opacity: 0.7,
},
});

77
apps/mobile/app/index.tsx Normal file
View File

@@ -0,0 +1,77 @@
import { Button } from "@/components/ui/Button";
import { rawTheme, ThemeName } from "@/lib/colors";
import useAuthStore from "@/store/auth";
import { Redirect, router } from "expo-router";
import { useColorScheme } from "nativewind";
import { View, Text, Dimensions, TouchableOpacity, Image } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import Svg, { Path } from "react-native-svg";
import Animated, { SlideInDown } from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";
export default function HomeScreen() {
const { auth } = useAuthStore();
const { colorScheme } = useColorScheme();
if (auth.session) {
return <Redirect href="/dashboard" />;
}
return (
<Animated.View
entering={SlideInDown.springify().damping(100).stiffness(300)}
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")}
className="w-[120px] h-[120px] mx-auto"
/>
<Text className="text-base-100 text-4xl font-semibold mt-7 mx-auto">
Linkwarden
</Text>
</View>
<View>
<Text className="text-base-100 text-xl text-center font-semibold mx-4 mt-3">
Welcome to the official mobile app for Linkwarden!
</Text>
<Text className="text-base-100 text-xl text-center mx-4 mt-3">
Expect regular improvements and new features as we continue refining
the experience.
</Text>
</View>
<Svg
viewBox="0 0 1440 320"
width={Dimensions.get("screen").width}
height={Dimensions.get("screen").width * (320 / 1440) + 2}
>
<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>
<SafeAreaView
edges={["bottom"]}
className="flex-col justify-end h-auto duration-100 pt-10 bg-base-100 -mt-2 pb-10 gap-4 w-full px-4"
>
<Button
variant="accent"
size="lg"
onPress={() => router.navigate("/login")}
>
<Text className="text-white text-xl">Get Started</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>
</SafeAreaView>
</View>
</Animated.View>
);
}

View File

@@ -0,0 +1,111 @@
import React, { useEffect, useState } from "react";
import { View, ActivityIndicator, Text, Platform } from "react-native";
import { WebView } from "react-native-webview";
import useAuthStore from "@/store/auth";
import { useLocalSearchParams } from "expo-router";
import { useUser } from "@linkwarden/router/user";
import { useGetLink } from "@linkwarden/router/links";
import useTmpStore from "@/store/tmp";
import { ArchivedFormat } from "@linkwarden/types";
import ReadableFormat from "@/components/Formats/ReadableFormat";
import ImageFormat from "@/components/Formats/ImageFormat";
import PdfFormat from "@/components/Formats/PdfFormat";
import WebpageFormat from "@/components/Formats/WebpageFormat";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function LinkScreen() {
const { auth } = useAuthStore();
const { id, format } = useLocalSearchParams();
const { data: user } = useUser(auth);
const [url, setUrl] = useState<string>();
const [isLoading, setIsLoading] = useState(true);
const { data: link } = useGetLink({ id: Number(id), auth, enabled: true });
const { updateTmp } = useTmpStore();
useEffect(() => {
if (link?.id && user?.id)
updateTmp({
link,
user: {
id: user.id,
},
});
return () =>
updateTmp({
link: null,
});
}, [link, user]);
useEffect(() => {
if (user?.id && link?.id && format) {
setUrl(`${auth.instance}/api/v1/archives/${link.id}?format=${format}`);
} else if (!url) {
if (link?.url) {
setUrl(link.url);
}
}
}, [user, link]);
const insets = useSafeAreaInsets();
return (
<View
className="flex-1"
style={{ paddingBottom: Platform.OS === "android" ? insets.bottom : 0 }}
>
{link?.id && Number(format) === ArchivedFormat.readability ? (
<ReadableFormat
link={link as any}
setIsLoading={(state) => setIsLoading(state)}
/>
) : link?.id &&
(Number(format) === ArchivedFormat.jpeg ||
Number(format) === ArchivedFormat.png) ? (
<ImageFormat
link={link as any}
setIsLoading={(state) => setIsLoading(state)}
format={Number(format)}
/>
) : link?.id && Number(format) === ArchivedFormat.pdf ? (
<PdfFormat
link={link as any}
setIsLoading={(state) => setIsLoading(state)}
/>
) : link?.id && Number(format) === ArchivedFormat.monolith ? (
<WebpageFormat
link={link as any}
setIsLoading={(state) => setIsLoading(state)}
/>
) : url ? (
<WebView
className={isLoading ? "opacity-0" : "flex-1"}
source={{
uri: url,
headers:
format || link?.type !== "url"
? { Authorization: `Bearer ${auth.session}` }
: {},
}}
onLoadEnd={() => setIsLoading(false)}
/>
) : (
<View className="flex-1 justify-center items-center bg-base-100 p-5">
<Text className="text-base text-neutral">
No link data available. Please check your network connection or try
again later.
</Text>
</View>
)}
{isLoading && (
<View className="absolute inset-0 flex-1 justify-center items-center bg-base-100 p-5">
<ActivityIndicator size="large" />
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
</View>
)}
</View>
);
}

197
apps/mobile/app/login.tsx Normal file
View File

@@ -0,0 +1,197 @@
import { Button } from "@/components/ui/Button";
import Input from "@/components/ui/Input";
import { rawTheme, ThemeName } from "@/lib/colors";
import useAuthStore from "@/store/auth";
import { Redirect } from "expo-router";
import { useColorScheme } from "nativewind";
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 {
KeyboardStickyView,
KeyboardToolbar,
} from "react-native-keyboard-controller";
import { SafeAreaView } from "react-native-safe-area-context";
export default function HomeScreen() {
const { auth, signIn } = useAuthStore();
const { colorScheme } = useColorScheme();
const [method, setMethod] = useState<"password" | "token">("password");
const [isLoading, setIsLoading] = useState(false);
const [form, setForm] = useState({
user: "",
password: "",
token: "",
instance: auth.instance || "https://cloud.linkwarden.app",
});
const [showInstanceField, setShowInstanceField] = useState(
form.instance !== "https://cloud.linkwarden.app"
);
useEffect(() => {
setForm((prev) => ({
...prev,
instance: auth.instance || "https://cloud.linkwarden.app",
}));
}, [auth.instance]);
useEffect(() => {
setShowInstanceField(form.instance !== "https://cloud.linkwarden.app");
}, [form.instance]);
useEffect(() => {
setForm((prev) => ({
...prev,
token: "",
user: "",
password: "",
}));
}, [method]);
if (auth.status === "authenticated") {
return <Redirect href="/dashboard" />;
}
return (
<>
<KeyboardStickyView className="flex-col justify-end h-full bg-base-100 relative">
<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={Dimensions.get("screen").width * (320 / 1440) + 2}
>
<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>
<SafeAreaView
edges={["bottom"]}
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>
<Button
variant="accent"
size="lg"
isLoading={isLoading}
onPress={async () => {
if (
((form.user && form.password) || form.token) &&
form.instance
) {
setIsLoading(true);
await signIn(
form.user,
form.password,
form.instance,
form.token
);
setIsLoading(false);
}
}}
>
<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>
</SafeAreaView>
</View>
</KeyboardStickyView>
<KeyboardToolbar />
</>
);
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

@@ -0,0 +1,9 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};

View File

@@ -0,0 +1,77 @@
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";
import { Button } from "@/components/ui/Button";
import { useAddLink } from "@linkwarden/router/links";
import useAuthStore from "@/store/auth";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function AddLinkSheet() {
const actionSheetRef = useRef<ActionSheetRef>(null);
const { auth } = useAuthStore();
const addLink = useAddLink(auth);
const [link, setLink] = useState("");
const { colorScheme } = useColorScheme();
const insets = useSafeAreaInsets();
return (
<ActionSheet
ref={actionSheetRef}
gestureEnabled
indicatorStyle={{
backgroundColor: rawTheme[colorScheme as ThemeName]["neutral-content"],
}}
containerStyle={{
backgroundColor: rawTheme[colorScheme as ThemeName]["base-200"],
}}
safeAreaInsets={insets}
>
<View className="px-8 py-5">
<Input
placeholder="e.g. https://example.com"
className="mb-4 bg-base-100"
value={link}
onChangeText={setLink}
/>
<Button
onPress={() =>
addLink.mutate(
{ url: link },
{
onSuccess: () => {
actionSheetRef.current?.hide();
setLink("");
},
onError: (error) => {
Alert.alert("Error", "There was an error adding the link.");
console.error("Error adding link:", error);
},
}
)
}
isLoading={addLink.isPending}
variant="accent"
className="mb-2"
>
<Text className="text-white">Save to Linkwarden</Text>
</Button>
<Button
onPress={() => {
actionSheetRef.current?.hide();
setLink("");
}}
variant="outline"
className="mb-2"
>
<Text className="text-base-content">Cancel</Text>
</Button>
</View>
</ActionSheet>
);
}

View File

@@ -0,0 +1,276 @@
import { View, Text, Alert } from "react-native";
import { useCallback, useEffect, useMemo, useState } from "react";
import ActionSheet, {
FlatList,
Route,
SheetManager,
SheetProps,
useSheetRouteParams,
useSheetRouter,
} from "react-native-actions-sheet";
import Input from "@/components/ui/Input";
import { Button } from "@/components/ui/Button";
import { useAddLink, useUpdateLink } from "@linkwarden/router/links";
import useAuthStore from "@/store/auth";
import {
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@linkwarden/types";
import { useCollections } from "@linkwarden/router/collections";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { Folder, ChevronRight, Check } from "lucide-react-native";
import useTmpStore from "@/store/tmp";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const Main = (props: SheetProps<"edit-link-sheet">) => {
const { auth } = useAuthStore();
const params = useSheetRouteParams("edit-link-sheet", "main");
const [link, setLink] = useState<
LinkIncludingShortenedCollectionAndTags | undefined
>(props.payload?.link);
const editLink = useUpdateLink(auth);
const router = useSheetRouter("edit-link-sheet");
const { colorScheme } = useColorScheme();
useEffect(() => {
if (params?.link) {
setLink(params.link);
}
}, [params?.link]);
const { tmp, updateTmp } = useTmpStore();
return (
<View className="px-8 py-5">
<Input
placeholder="Name"
className="mb-4 bg-base-100"
value={link?.name || ""}
onChangeText={(text) => link?.id && setLink({ ...link, name: text })}
/>
{props.payload?.link?.url && (
<Input
placeholder="URL"
className="mb-4 bg-base-100"
value={link?.url || ""}
onChangeText={(text) => link?.id && setLink({ ...link, url: text })}
/>
)}
<Button
variant="input"
className="mb-4"
onPress={() => router?.navigate("collections", { link })}
>
<View className="flex-row items-center gap-2 w-[90%]">
<Folder
size={20}
fill={link?.collection.color || "gray"}
color={link?.collection.color || "gray"}
/>
<Text numberOfLines={1} className="w-[90%] text-base-content">
{link?.collection.name}
</Text>
</View>
<ChevronRight
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
</Button>
{/* <Button variant="input" className="mb-4 h-auto">
{link?.tags && link?.tags.length > 0 ? (
<View className="flex-row flex-wrap items-center gap-2 w-[90%]">
{link.tags.map((tag) => (
<View
key={tag.id}
className="bg-gray-200 rounded-md h-7 px-2 py-1"
>
<Text numberOfLines={1}>{tag.name}</Text>
</View>
))}
</View>
) : (
<Text className="text-gray-500">No tags</Text>
)}
<ChevronRight size={16} color={"gray"} />
</Button> */}
<Input
multiline
textAlignVertical="top"
placeholder="Description"
className="mb-4 h-28 bg-base-100"
value={link?.description || ""}
onChangeText={(text) =>
link?.id && setLink({ ...link, description: text })
}
/>
<Button
onPress={() =>
editLink.mutate(link as LinkIncludingShortenedCollectionAndTags, {
onSuccess: () => {
if (link && tmp.link)
updateTmp({
link,
});
SheetManager.hide("edit-link-sheet");
},
onError: (error) => {
Alert.alert("Error", "There was an error editing the link.");
console.error("Error editing link:", error);
},
})
}
isLoading={editLink.isPending}
variant="accent"
className="mb-2"
>
<Text className="text-white">Save</Text>
</Button>
<Button
onPress={() => {
SheetManager.hide("edit-link-sheet");
}}
variant="outline"
className="mb-2"
>
<Text className="text-base-content">Cancel</Text>
</Button>
</View>
);
};
const Collections = () => {
const { auth } = useAuthStore();
const addLink = useAddLink(auth);
const [searchQuery, setSearchQuery] = useState("");
const router = useSheetRouter("edit-link-sheet");
const { link: currentLink } = useSheetRouteParams<
"edit-link-sheet",
"collections"
>("edit-link-sheet", "collections");
const params = useSheetRouteParams("edit-link-sheet", "collections");
const collections = useCollections(auth);
const { colorScheme } = useColorScheme();
const filteredCollections = useMemo(() => {
if (!collections.data) return [];
const q = searchQuery.trim().toLowerCase();
if (q === "") return collections.data;
return collections.data.filter((col) => col.name.toLowerCase().includes(q));
}, [collections.data, searchQuery]);
const renderItem = useCallback(
({
item: collection,
}: {
item: CollectionIncludingMembersAndLinkCount;
}) => {
const onSelect = () => {
// 1. Create a brand-new link object with the new collection
const updatedLink = {
...currentLink!,
collection,
};
// 2. Navigate back to "main", passing the updated link as payload
router?.popToTop();
router?.navigate("main", { link: updatedLink });
};
return (
<Button variant="input" className="mb-2" onPress={onSelect}>
<View className="flex-row items-center gap-2 w-[75%]">
<Folder
size={20}
fill={collection.color || "gray"}
color={collection.color || "gray"}
/>
<Text numberOfLines={1} className="w-full text-base-content">
{collection.name}
</Text>
</View>
<View className="flex-row items-center gap-2">
{params.link?.collection.id === collection.id && (
<Check
size={16}
color={rawTheme[colorScheme as ThemeName].primary}
/>
)}
<Text className="text-neutral">
{collection._count?.links ?? 0}
</Text>
</View>
</Button>
);
},
[addLink, params.link, router]
);
return (
<View className="px-8 py-5 max-h-[80vh]">
<Input
placeholder="Search collections"
className="mb-4 bg-base-100"
value={searchQuery}
onChangeText={setSearchQuery}
/>
<FlatList
data={filteredCollections}
keyExtractor={(e, i) => i.toString()}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{ textAlign: "center", marginTop: 20 }}
className="text-neutral"
>
No collections match {searchQuery}
</Text>
}
contentContainerStyle={{ paddingBottom: 20 }}
/>
</View>
);
};
const routes: Route[] = [
{
name: "main",
component: Main,
},
{
name: "collections",
component: Collections,
},
];
export default function EditLinkSheet() {
const { colorScheme } = useColorScheme();
const insets = useSafeAreaInsets();
return (
<ActionSheet
gestureEnabled
indicatorStyle={{
backgroundColor: rawTheme[colorScheme as ThemeName]["neutral-content"],
}}
enableRouterBackNavigation={true}
routes={routes}
initialRoute="main"
containerStyle={{
backgroundColor: rawTheme[colorScheme as ThemeName]["base-200"],
}}
safeAreaInsets={insets}
/>
);
}

View File

@@ -0,0 +1,92 @@
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";
import { Button } from "@/components/ui/Button";
import useAuthStore from "@/store/auth";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { useCreateCollection } from "@linkwarden/router/collections";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function NewCollectionSheet() {
const actionSheetRef = useRef<ActionSheetRef>(null);
const { auth } = useAuthStore();
const createCollection = useCreateCollection(auth);
const [collection, setCollection] = useState({
name: "",
description: "",
});
const { colorScheme } = useColorScheme();
const insets = useSafeAreaInsets();
return (
<ActionSheet
ref={actionSheetRef}
gestureEnabled
indicatorStyle={{
backgroundColor: rawTheme[colorScheme as ThemeName]["neutral-content"],
}}
containerStyle={{
backgroundColor: rawTheme[colorScheme as ThemeName]["base-200"],
}}
safeAreaInsets={insets}
>
<View className="px-8 py-5">
<Input
placeholder="Name"
className="mb-4 bg-base-100"
value={collection.name}
onChangeText={(text) => setCollection({ ...collection, name: text })}
/>
<Input
placeholder="Description"
className="mb-4 bg-base-100"
value={collection.description}
onChangeText={(text) =>
setCollection({ ...collection, description: text })
}
/>
<Button
onPress={() =>
createCollection.mutate(
{ name: collection.name, description: collection.description },
{
onSuccess: () => {
actionSheetRef.current?.hide();
setCollection({ name: "", description: "" });
},
onError: (error) => {
Alert.alert(
"Error",
"There was an error creating the collection."
);
console.error("Error creating collection:", error);
},
}
)
}
isLoading={createCollection.isPending}
variant="accent"
className="mb-2"
>
<Text className="text-white">Save Collection</Text>
</Button>
<Button
onPress={() => {
actionSheetRef.current?.hide();
setCollection({ name: "", description: "" });
}}
variant="outline"
className="mb-2"
>
<Text className="text-base-content">Cancel</Text>
</Button>
</View>
</ActionSheet>
);
}

View File

@@ -0,0 +1,38 @@
import {
registerSheet,
RouteDefinition,
SheetDefinition,
} from "react-native-actions-sheet";
import SupportSheet from "./SupportSheet";
import AddLinkSheet from "./AddLinkSheet";
import EditLinkSheet from "./EditLinkSheet";
import NewCollectionSheet from "./NewCollectionSheet";
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
registerSheet("support-sheet", SupportSheet);
registerSheet("add-link-sheet", AddLinkSheet);
registerSheet("edit-link-sheet", EditLinkSheet);
registerSheet("new-collection-sheet", NewCollectionSheet);
declare module "react-native-actions-sheet" {
interface Sheets {
"support-sheet": SheetDefinition;
"add-link-sheet": SheetDefinition;
"edit-link-sheet": SheetDefinition<{
payload: {
link: LinkIncludingShortenedCollectionAndTags;
};
routes: {
main: RouteDefinition<{
link: LinkIncludingShortenedCollectionAndTags;
}>;
collections: RouteDefinition<{
link: LinkIncludingShortenedCollectionAndTags;
}>;
};
}>;
"new-collection-sheet": SheetDefinition;
}
}
export {};

View File

@@ -0,0 +1,47 @@
import { Text, View } from "react-native";
import { useState } from "react";
import ActionSheet from "react-native-actions-sheet";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import * as Clipboard from "expo-clipboard";
import { Button } from "../ui/Button";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function SupportSheet() {
const { colorScheme } = useColorScheme();
const [copied, setCopied] = useState(false);
async function handleEmailPress() {
await Clipboard.setStringAsync("support@linkwarden.app");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
const insets = useSafeAreaInsets();
return (
<ActionSheet
gestureEnabled
indicatorStyle={{
backgroundColor: rawTheme[colorScheme as ThemeName]["neutral-content"],
}}
containerStyle={{
backgroundColor: rawTheme[colorScheme as ThemeName]["base-100"],
}}
safeAreaInsets={insets}
>
<View className="px-8 py-5 flex-col gap-4">
<Text className="text-2xl font-bold text-base-content">Need help?</Text>
<Text className="text-base-content">
Whether you have a question or need assistance, feel free to reach out
to us at support@linkwarden.app
</Text>
<Button onPress={handleEmailPress} variant="outline">
<Text className="text-base-content">
{copied ? "Copied!" : "Copy Support Email"}
</Text>
</Button>
</View>
</ActionSheet>
);
}

View File

@@ -0,0 +1,132 @@
import { View, Text, Pressable, Platform, Alert } from "react-native";
import { decode } from "html-entities";
import { CollectionIncludingMembersAndLinkCount } from "@linkwarden/types";
import useAuthStore from "@/store/auth";
import { useRouter } from "expo-router";
import * as ContextMenu from "zeego/context-menu";
import { cn } from "@linkwarden/lib/utils";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { CalendarDays, Folder, Link } from "lucide-react-native";
import { useDeleteCollection } from "@linkwarden/router/collections";
type Props = {
collection: CollectionIncludingMembersAndLinkCount;
};
const CollectionListing = ({ collection }: Props) => {
const { auth } = useAuthStore();
const router = useRouter();
const { colorScheme } = useColorScheme();
const deleteCollection = useDeleteCollection(auth);
return (
<ContextMenu.Root>
<ContextMenu.Trigger asChild>
<Pressable
className={cn(
"p-5 flex-row justify-between",
"bg-base-100",
Platform.OS !== "android" && "active:bg-base-200/50"
)}
onLongPress={() => {}}
onPress={() => router.navigate(`/collections/${collection.id}`)}
android_ripple={{
color: colorScheme === "dark" ? "rgba(255,255,255,0.2)" : "#ddd",
borderless: false,
}}
>
<View className="w-full">
<View className="w-[90%] flex-col justify-between gap-3">
<View className="flex flex-row gap-2 items-center pr-1.5 self-start rounded-md">
<Folder
size={16}
fill={collection.color || ""}
color={collection.color || ""}
/>
<Text
numberOfLines={2}
className="font-medium text-lg text-base-content"
>
{decode(collection.name)}
</Text>
</View>
{collection.description && (
<Text
numberOfLines={2}
className="font-light text-sm text-base-content"
>
{decode(collection.description)}
</Text>
)}
</View>
<View className="flex-row gap-3">
<View className="flex flex-row gap-1 items-center mt-5 self-start">
<CalendarDays
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
<Text
numberOfLines={1}
className="font-light text-xs text-base-content"
>
{new Date(collection.createdAt as string).toLocaleString(
"en-US",
{
year: "numeric",
month: "short",
day: "numeric",
}
)}
</Text>
</View>
<View className="flex flex-row gap-1 items-center mt-5 self-start">
<Link
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
<Text
numberOfLines={1}
className="font-light text-xs text-base-content"
>
{collection._count?.links}
</Text>
</View>
</View>
</View>
</Pressable>
</ContextMenu.Trigger>
<ContextMenu.Content avoidCollisions>
<ContextMenu.Item
key="delete-collection"
onSelect={() => {
return Alert.alert(
"Delete Collection",
"Are you sure you want to delete this collection? This action cannot be undone.",
[
{
text: "Cancel",
style: "cancel",
},
{
text: "Delete",
style: "destructive",
onPress: () => {
deleteCollection.mutate(collection.id as number);
},
},
]
);
}}
>
<ContextMenu.ItemTitle>Delete</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
);
};
export default CollectionListing;

View File

@@ -0,0 +1,35 @@
import { Text, View } from "react-native";
export default function DashboardItem({
name,
value,
icon,
color,
}: {
name: string;
value: number;
icon: React.ReactNode;
color: string;
}) {
return (
<View className="flex-1 flex-col gap-2 rounded-xl bg-base-200 p-3">
<View className="flex-row justify-between">
<View
className="flex-col gap-2 rounded-full aspect-square flex justify-center items-center"
style={{
backgroundColor: color,
}}
>
{icon}
</View>
<Text
className="text-4xl text-base-content mt-0.5 text-right max-w-[75%]"
numberOfLines={1}
>
{value || 0}
</Text>
</View>
<Text className="font-semibold text-neutral">{name}</Text>
</View>
);
}

View File

@@ -0,0 +1,346 @@
import {
FlatList,
Text,
TouchableOpacity,
View,
ViewToken,
} from "react-native";
import React from "react";
import clsx from "clsx";
import DashboardItem from "@/components/DashboardItem";
import { rawTheme, ThemeName } from "@/lib/colors";
import {
Clock8,
ChevronRight,
Pin,
Folder,
Hash,
Link,
} from "lucide-react-native";
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
import LinkListing from "@/components/LinkListing";
import { useColorScheme } from "nativewind";
import { useRouter } from "expo-router";
// Don't use prisma client's DashboardSectionType, it'll crash in production (React Native)
type DashboardSectionType =
| "STATS"
| "RECENT_LINKS"
| "PINNED_LINKS"
| "COLLECTION";
type DashboardSectionProps = {
sectionData: { type: DashboardSectionType };
collection?: any;
links?: any[];
tagsLength: number;
numberOfLinks: number;
collectionsLength: number;
numberOfPinnedLinks: number;
dashboardData: {
isLoading: boolean;
refetch: Function;
isRefetching: boolean;
};
collectionLinks?: any[];
};
const DashboardSection: React.FC<DashboardSectionProps> = ({
sectionData,
collection,
links = [],
tagsLength,
numberOfLinks,
collectionsLength,
numberOfPinnedLinks,
dashboardData,
collectionLinks = [],
}) => {
const { colorScheme } = useColorScheme();
const router = useRouter();
switch (sectionData.type) {
case "STATS":
return (
<View className="flex-col gap-4 max-w-full px-5">
<View className="flex-row gap-4">
<DashboardItem
name={numberOfLinks === 1 ? "Link" : "Links"}
value={numberOfLinks}
icon={<Link size={23} color="white" />}
color="#9c00cc"
/>
<DashboardItem
name={collectionsLength === 1 ? "Collection" : "Collections"}
value={collectionsLength}
icon={<Folder size={23} color="white" fill="white" />}
color="#0096cc"
/>
</View>
<View className="flex-row gap-4">
<DashboardItem
name={tagsLength === 1 ? "Tag" : "Tags"}
value={tagsLength}
icon={<Hash size={23} color="white" />}
color="#00cc99"
/>
<DashboardItem
name={"Pinned Links"}
value={numberOfPinnedLinks}
icon={<Pin size={23} color="white" fill="white" />}
color="#cc6d00"
/>
</View>
</View>
);
case "RECENT_LINKS":
return (
<>
<View className="flex-row justify-between items-center px-5">
<View className="flex-row gap-2 items-center">
<View className={"flex-row items-center gap-2"}>
<Clock8
size={30}
color={rawTheme[colorScheme as ThemeName].primary}
/>
<Text className="text-2xl capitalize text-base-content">
Recent Links
</Text>
</View>
</View>
<TouchableOpacity
className="flex-row items-center text-sm gap-1"
onPress={() => router.navigate("/(tabs)/dashboard/recent-links")}
>
<Text className="text-primary">View All</Text>
<ChevronRight
size={15}
color={rawTheme[colorScheme as ThemeName].primary}
/>
</TouchableOpacity>
</View>
{dashboardData.isLoading ||
(links.length > 0 && !dashboardData.isLoading) ? (
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
directionalLockEnabled
data={links || []}
refreshing={dashboardData.isLoading}
initialNumToRender={2}
keyExtractor={(item) => item.id?.toString() || ""}
renderItem={({ item }) => (
<RenderItem item={item} key={item.id?.toString()} />
)}
ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
contentContainerStyle={{
paddingHorizontal: 20,
}}
onViewableItemsChanged={({
viewableItems,
}: {
viewableItems: ViewToken[];
}) => {
const links = viewableItems.map(
(e) => e.item
) as LinkIncludingShortenedCollectionAndTags[];
if (
!dashboardData.isRefetching &&
links.some((e) => e.id && !e.preview)
) {
dashboardData.refetch();
}
}}
viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
/>
) : (
<View className="flex-col gap-2 justify-center items-center h-40 p-10 rounded-xl bg-base-200 mx-5">
<Clock8
size={40}
color={rawTheme[colorScheme as ThemeName].primary}
/>
<Text className="text-center text-xl text-neutral">
No Recent Links
</Text>
{/* <View className="text-center w-full mt-4 flex-row flex-wrap gap-4 justify-center">
<Button onPress={() => setNewLinkModal(true)} variant="accent">
<Icon name="bi-plus-lg" className="text-xl" />
<Text>{t("add_link")}</Text>
</Button>
<ImportDropdown />
</View> */}
</View>
)}
</>
);
case "PINNED_LINKS":
return (
<>
<View className="flex-row justify-between items-center px-5">
<View className="flex-row gap-2 items-center">
<View className={"flex-row items-center gap-2"}>
<Pin
size={30}
color={rawTheme[colorScheme as ThemeName].primary}
/>
<Text className="text-2xl capitalize text-base-content">
Pinned Links
</Text>
</View>
</View>
<TouchableOpacity
className="flex-row items-center text-sm gap-1"
onPress={() => router.navigate("/(tabs)/dashboard/pinned-links")}
>
<Text className="text-primary">View All</Text>
<ChevronRight
size={15}
color={rawTheme[colorScheme as ThemeName].primary}
/>
</TouchableOpacity>
</View>
{dashboardData.isLoading ||
links?.some((e: any) => e.pinnedBy && e.pinnedBy[0]) ? (
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={links.filter((e: any) => e.pinnedBy && e.pinnedBy[0]) || []}
refreshing={dashboardData.isLoading}
initialNumToRender={2}
keyExtractor={(item) => item.id?.toString() || ""}
renderItem={({ item }) => (
<RenderItem item={item} key={item.id?.toString()} />
)}
ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
contentContainerStyle={{
paddingHorizontal: 20,
}}
onViewableItemsChanged={({
viewableItems,
}: {
viewableItems: ViewToken[];
}) => {
const links = viewableItems.map(
(e) => e.item
) as LinkIncludingShortenedCollectionAndTags[];
if (
!dashboardData.isRefetching &&
links.some((e) => e.id && !e.preview)
) {
dashboardData.refetch();
}
}}
viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
/>
) : (
<View className="flex-col gap-2 justify-center items-center h-40 p-10 rounded-xl bg-base-200 mx-5">
<Pin
size={40}
color={rawTheme[colorScheme as ThemeName].primary}
/>
<Text className="text-center text-xl text-neutral">
No Pinned Links
</Text>
</View>
)}
</>
);
case "COLLECTION":
return collection?.id ? (
<>
<View className="flex-row justify-between items-center px-5">
<View className="flex-row gap-2 items-center max-w-[60%]">
<View className={clsx("flex-row items-center gap-2")}>
<Folder
size={30}
fill={collection.color || "#0ea5e9"}
color={collection.color || "#0ea5e9"}
/>
<Text
className="text-2xl capitalize w-full text-base-content"
numberOfLines={1}
>
{collection.name}
</Text>
</View>
</View>
<TouchableOpacity
className="flex-row items-center text-sm gap-1 whitespace-nowrap"
onPress={() =>
router.navigate(
`/(tabs)/dashboard/collection?collectionId=${collection.id}`
)
}
>
<Text className="text-primary">View All</Text>
<ChevronRight
size={15}
color={rawTheme[colorScheme as ThemeName].primary}
/>
</TouchableOpacity>
</View>
{dashboardData.isLoading || collectionLinks.length > 0 ? (
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={collectionLinks || []}
refreshing={dashboardData.isLoading}
initialNumToRender={2}
keyExtractor={(item) => item.id?.toString() || ""}
renderItem={({ item }) => (
<RenderItem item={item} key={item.id?.toString()} />
)}
ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
contentContainerStyle={{
paddingHorizontal: 20,
}}
onViewableItemsChanged={({
viewableItems,
}: {
viewableItems: ViewToken[];
}) => {
const links = viewableItems.map(
(e) => e.item
) as LinkIncludingShortenedCollectionAndTags[];
if (
!dashboardData.isRefetching &&
links.some((e) => e.id && !e.preview)
) {
dashboardData.refetch();
}
}}
viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
/>
) : (
<View className="flex-col gap-2 justify-center items-center h-40 p-10 rounded-xl bg-base-200 mx-5">
<Text className="text-center text-xl text-neutral">
Empty Collection
</Text>
</View>
)}
</>
) : null;
default:
return null;
}
};
export default DashboardSection;
const RenderItem = React.memo(
({ item }: { item: LinkIncludingShortenedCollectionAndTags }) => {
return <LinkListing link={item} dashboard />;
}
);

View File

@@ -0,0 +1,18 @@
import { View, Text, Button } from "react-native";
export default function ElementNotSupported({
message = "This element is currently not supported in this view.",
buttonTitle = "Open original",
onPress,
}: {
message?: string;
buttonTitle?: string;
onPress: () => void;
}) {
return (
<View className="border-y border-neutral-content my-2 py-5 flex justify-center items-center">
<Text className="text-neutral">{message}</Text>
<Button onPress={onPress} title={buttonTitle} />
</View>
);
}

View File

@@ -0,0 +1,136 @@
import React, { useEffect, useState } from "react";
import * as FileSystem from "expo-file-system";
import NetInfo from "@react-native-community/netinfo";
import useAuthStore from "@/store/auth";
import { ArchivedFormat } from "@linkwarden/types";
import { Link as LinkType } from "@linkwarden/prisma/client";
import WebView from "react-native-webview";
import { Image, Platform, ScrollView } from "react-native";
type Props = {
link: LinkType;
setIsLoading: (state: boolean) => void;
format: ArchivedFormat.png | ArchivedFormat.jpeg;
};
export default function ImageFormat({ link, setIsLoading, format }: Props) {
const FORMAT = format;
const extension = format === ArchivedFormat.png ? "png" : "jpeg";
const { auth } = useAuthStore();
const [content, setContent] = useState<string>("");
const [dimension, setDimension] = useState<{
width: number;
height: number;
}>();
useEffect(() => {
if (content)
Image.getSize(content, (width, height) => {
setDimension({ width, height });
});
}, [content]);
useEffect(() => {
async function loadCacheOrFetch() {
const filePath =
FileSystem.documentDirectory +
`archivedData/${extension}/link_${link.id}.${extension}`;
await FileSystem.makeDirectoryAsync(
filePath.substring(0, filePath.lastIndexOf("/")),
{
intermediates: true,
}
).catch(() => {});
const [info] = await Promise.all([FileSystem.getInfoAsync(filePath)]);
if (info.exists) {
setContent(filePath);
}
const net = await NetInfo.fetch();
if (net.isConnected) {
const apiUrl = `${auth.instance}/api/v1/archives/${link.id}?format=${FORMAT}`;
try {
const result = await FileSystem.downloadAsync(apiUrl, filePath, {
headers: { Authorization: `Bearer ${auth.session}` },
});
setContent(result.uri);
} catch (e) {
console.error("Failed to fetch content", e);
}
}
}
loadCacheOrFetch();
}, [link]);
if (Platform.OS === "ios")
return (
content &&
dimension && (
<ScrollView maximumZoomScale={10}>
<Image
source={{ uri: content }}
onLoadEnd={() => setIsLoading(false)}
style={{
width: "100%",
height: "auto",
aspectRatio: dimension.width / dimension.height,
}}
resizeMode="contain"
/>
</ScrollView>
)
);
else
return (
content && (
<WebView
style={{
flex: 1,
}}
source={{
baseUrl: content,
html: `
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
}
img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
<img src="${content}" />
</body>
</html>
`,
}}
scalesPageToFit
originWhitelist={["*"]}
mixedContentMode="always"
javaScriptEnabled={true}
allowFileAccess={true}
allowFileAccessFromFileURLs={true}
allowUniversalAccessFromFileURLs={true}
onLoadEnd={() => setIsLoading(false)}
/>
)
);
}

View File

@@ -0,0 +1,72 @@
import React, { useEffect, useState } from "react";
import * as FileSystem from "expo-file-system";
import NetInfo from "@react-native-community/netinfo";
import useAuthStore from "@/store/auth";
import { ArchivedFormat } from "@linkwarden/types";
import { Link as LinkType } from "@linkwarden/prisma/client";
import Pdf from "react-native-pdf";
type Props = {
link: LinkType;
setIsLoading: (state: boolean) => void;
};
export default function PdfFormat({ link, setIsLoading }: Props) {
const FORMAT = ArchivedFormat.pdf;
const { auth } = useAuthStore();
const [content, setContent] = useState<string>("");
useEffect(() => {
async function loadCacheOrFetch() {
const filePath =
FileSystem.documentDirectory + `archivedData/pdf/link_${link.id}.pdf`;
await FileSystem.makeDirectoryAsync(
filePath.substring(0, filePath.lastIndexOf("/")),
{
intermediates: true,
}
).catch(() => {});
const [info] = await Promise.all([FileSystem.getInfoAsync(filePath)]);
if (info.exists) {
setContent(filePath);
}
const net = await NetInfo.fetch();
if (net.isConnected) {
const apiUrl = `${auth.instance}/api/v1/archives/${link.id}?format=${FORMAT}`;
try {
const result = await FileSystem.downloadAsync(apiUrl, filePath, {
headers: { Authorization: `Bearer ${auth.session}` },
});
setContent(result.uri);
} catch (e) {
console.error("Failed to fetch content", e);
}
}
}
loadCacheOrFetch();
}, [link]);
return (
content && (
<Pdf
style={{
flex: 1,
}}
source={{ uri: content }}
onLoadComplete={() => setIsLoading(false)}
onPressLink={(uri) => {
console.log(`Link pressed: ${uri}`);
}}
/>
)
);
}

View File

@@ -0,0 +1,139 @@
import React, { useEffect, useState } from "react";
import { View, Text, ScrollView, TouchableOpacity } from "react-native";
import * as FileSystem from "expo-file-system";
import NetInfo from "@react-native-community/netinfo";
import useAuthStore from "@/store/auth";
import { useRouter } from "expo-router";
import { useWindowDimensions } from "react-native";
import RenderHtml from "@linkwarden/react-native-render-html";
import ElementNotSupported from "@/components/ElementNotSupported";
import { decode } from "html-entities";
import { useColorScheme } from "nativewind";
import { rawTheme, ThemeName } from "@/lib/colors";
import { CalendarDays, Link } from "lucide-react-native";
import { ArchivedFormat } from "@linkwarden/types";
import { Link as LinkType } from "@linkwarden/prisma/client";
type Props = {
link: LinkType;
setIsLoading: (state: boolean) => void;
};
export default function ReadableFormat({ link, setIsLoading }: Props) {
const FORMAT = ArchivedFormat.readability;
const { auth } = useAuthStore();
const [content, setContent] = useState<string>("");
const { width } = useWindowDimensions();
const router = useRouter();
const { colorScheme } = useColorScheme();
useEffect(() => {
async function loadCacheOrFetch() {
const filePath =
FileSystem.documentDirectory +
`archivedData/readable/link_${link.id}.html`;
await FileSystem.makeDirectoryAsync(
filePath.substring(0, filePath.lastIndexOf("/")),
{
intermediates: true,
}
).catch(() => {});
const [info] = await Promise.all([FileSystem.getInfoAsync(filePath)]);
if (info.exists) {
const rawContent = await FileSystem.readAsStringAsync(filePath);
setContent(rawContent);
}
const net = await NetInfo.fetch();
if (net.isConnected) {
const apiUrl = `${auth.instance}/api/v1/archives/${link.id}?format=${FORMAT}`;
try {
const response = await fetch(apiUrl, {
headers: { Authorization: `Bearer ${auth.session}` },
});
const data = (await response.json()).content;
setContent(data);
await FileSystem.writeAsStringAsync(filePath, data, {
encoding: FileSystem.EncodingType.UTF8,
});
} catch (e) {
console.error("Failed to fetch content", e);
}
}
}
loadCacheOrFetch();
}, [link]);
return (
content && (
<ScrollView
className="flex-1 bg-base-100"
contentContainerClassName="p-4"
nestedScrollEnabled
>
<Text className="text-2xl font-bold mb-2.5 text-base-content">
{decode(link.name || link.description || link.url || "")}
</Text>
<TouchableOpacity
className="flex-row items-center gap-1 mb-2.5 pr-5"
onPress={() => router.replace(`/links/${link.id}`)}
>
<Link
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
<Text className="text-base text-neutral flex-1" numberOfLines={1}>
{link.url}
</Text>
</TouchableOpacity>
<View className="flex-row items-center gap-1 mb-2.5">
<CalendarDays
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
<Text className="text-base text-neutral">
{new Date(link?.importDate || link.createdAt).toLocaleString(
"en-US",
{
year: "numeric",
month: "short",
day: "numeric",
}
)}
</Text>
</View>
<View className="border-t border-neutral-content mt-2.5 mb-5" />
<RenderHtml
contentWidth={width}
source={{ html: content }}
renderers={{
table: () => (
<ElementNotSupported
onPress={() => router.replace(`/links/${link.id}`)}
/>
),
}}
onHTMLLoaded={() => setIsLoading(false)}
tagsStyles={{
p: { fontSize: 18, lineHeight: 28, marginVertical: 10 },
}}
baseStyle={{
color: rawTheme[colorScheme as ThemeName]["base-content"],
}}
/>
</ScrollView>
)
);
}

View File

@@ -0,0 +1,80 @@
import React, { useEffect, useState } from "react";
import * as FileSystem from "expo-file-system";
import NetInfo from "@react-native-community/netinfo";
import useAuthStore from "@/store/auth";
import { ArchivedFormat } from "@linkwarden/types";
import { Link as LinkType } from "@linkwarden/prisma/client";
import WebView from "react-native-webview";
type Props = {
link: LinkType;
setIsLoading: (state: boolean) => void;
};
export default function WebpageFormat({ link, setIsLoading }: Props) {
const FORMAT = ArchivedFormat.monolith;
const { auth } = useAuthStore();
const [content, setContent] = useState<string>("");
useEffect(() => {
async function loadCacheOrFetch() {
const filePath =
FileSystem.documentDirectory +
`archivedData/webpage/link_${link.id}.html`;
await FileSystem.makeDirectoryAsync(
filePath.substring(0, filePath.lastIndexOf("/")),
{
intermediates: true,
}
).catch(() => {});
const [info] = await Promise.all([FileSystem.getInfoAsync(filePath)]);
if (info.exists) {
setContent(filePath);
}
const net = await NetInfo.fetch();
if (net.isConnected) {
const apiUrl = `${auth.instance}/api/v1/archives/${link.id}?format=${FORMAT}`;
try {
const result = await FileSystem.downloadAsync(apiUrl, filePath, {
headers: { Authorization: `Bearer ${auth.session}` },
});
setContent(result.uri);
} catch (e) {
console.error("Failed to fetch content", e);
}
}
}
loadCacheOrFetch();
}, [link]);
return (
content && (
<WebView
style={{
flex: 1,
}}
source={{
uri: content,
baseUrl: FileSystem.documentDirectory,
}}
scalesPageToFit
originWhitelist={["*"]}
mixedContentMode="always"
javaScriptEnabled={true}
allowFileAccess={true}
allowFileAccessFromFileURLs={true}
allowUniversalAccessFromFileURLs={true}
onLoadEnd={() => setIsLoading(false)}
/>
)
);
}

View File

@@ -0,0 +1,18 @@
import { BottomTabBarButtonProps } from "@react-navigation/bottom-tabs";
import { PlatformPressable } from "@react-navigation/elements";
import * as Haptics from "expo-haptics";
export default function HapticTab(props: BottomTabBarButtonProps) {
return (
<PlatformPressable
{...props}
onPressIn={(ev) => {
if (process.env.EXPO_OS === "ios") {
// Add a soft haptic feedback when pressing down on the tabs.
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
props.onPressIn?.(ev);
}}
/>
);
}

View File

@@ -0,0 +1,341 @@
import {
View,
Text,
Image,
Pressable,
Platform,
Alert,
ActivityIndicator,
Linking,
} from "react-native";
import { decode } from "html-entities";
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
import { ArchivedFormat } from "@linkwarden/types";
import getFormatBasedOnPreference from "@linkwarden/lib/getFormatBasedOnPreference";
import getOriginalFormat from "@linkwarden/lib/getOriginalFormat";
import {
atLeastOneFormatAvailable,
formatAvailable,
} from "@linkwarden/lib/formatStats";
import useAuthStore from "@/store/auth";
import { useRouter } from "expo-router";
import * as ContextMenu from "zeego/context-menu";
import { useDeleteLink, useUpdateLink } from "@linkwarden/router/links";
import { SheetManager } from "react-native-actions-sheet";
import * as Clipboard from "expo-clipboard";
import { cn } from "@linkwarden/lib/utils";
import { useUser } from "@linkwarden/router/user";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { CalendarDays, Folder } from "lucide-react-native";
import useDataStore from "@/store/data";
import { useEffect, useState } from "react";
import { deleteLinkCache } from "@/lib/cache";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
dashboard?: boolean;
};
const LinkListing = ({ link, dashboard }: Props) => {
const { auth } = useAuthStore();
const router = useRouter();
const updateLink = useUpdateLink(auth);
const { data: user } = useUser(auth);
const { colorScheme } = useColorScheme();
const { data } = useDataStore();
const deleteLink = useDeleteLink(auth);
const [url, setUrl] = useState("");
useEffect(() => {
try {
if (link.url) {
setUrl(new URL(link.url).host.toLowerCase());
}
} catch (error) {
console.log(error);
}
}, [link]);
return (
<ContextMenu.Root>
<ContextMenu.Trigger asChild>
<Pressable
className={cn(
"p-5 flex-row justify-between",
dashboard ? "bg-base-200" : "bg-base-100",
Platform.OS !== "android" && "active:bg-base-200/50",
dashboard && "rounded-xl"
)}
onLongPress={() => {}}
onPress={() => {
if (user) {
const format = getFormatBasedOnPreference({
link,
preference: user.linksRouteTo,
});
data.preferredBrowser === "app"
? router.navigate(
format !== null
? `/links/${link.id}?format=${format}`
: `/links/${link.id}`
)
: Linking.openURL(
format !== null
? auth.instance +
`/preserved/${link?.id}?format=${format}`
: (link.url as string)
);
}
}}
android_ripple={{
color: colorScheme === "dark" ? "rgba(255,255,255,0.2)" : "#ddd",
borderless: false,
}}
>
<View
className={cn(
"flex-row justify-between",
dashboard ? "w-80" : "w-full"
)}
>
<View className="w-[65%] flex-col justify-between">
<Text
numberOfLines={2}
className="font-medium text-lg text-base-content"
>
{decode(link.name || link.description || link.url)}
</Text>
{url && (
<Text
numberOfLines={1}
className="mt-1.5 font-light text-sm text-base-content"
>
{url}
</Text>
)}
<View className="flex flex-row gap-1 items-center mt-1.5 pr-1.5 self-start rounded-md">
<Folder
size={16}
fill={link.collection.color || ""}
color={link.collection.color || ""}
/>
<Text
numberOfLines={1}
className="font-light text-xs text-base-content"
>
{link.collection.name}
</Text>
</View>
</View>
<View className="flex-col items-end">
<View className="rounded-lg overflow-hidden relative">
{formatAvailable(link, "preview") ? (
<Image
source={{
uri: `${auth.instance}/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true&updatedAt=${link.updatedAt}`,
headers: {
Authorization: `Bearer ${auth.session}`,
},
}}
className="rounded-md h-[60px] w-[90px] object-cover scale-105"
/>
) : !link.preview ? (
<ActivityIndicator
size="small"
className="h-[60px] w-[90px]"
/>
) : (
<View className="h-[60px] w-[90px]" />
)}
</View>
<View className="flex flex-row gap-1 items-center mt-5 self-start">
<CalendarDays
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
<Text
numberOfLines={1}
className="font-light text-xs text-base-content"
>
{new Date(link.createdAt as string).toLocaleString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})}
</Text>
</View>
</View>
</View>
</Pressable>
</ContextMenu.Trigger>
<ContextMenu.Content avoidCollisions>
<ContextMenu.Item
key="open-original"
onSelect={() => {
if (link) {
const format = getOriginalFormat(link);
data.preferredBrowser === "app"
? router.navigate(
format !== null
? `/links/${link.id}?format=${format}`
: `/links/${link.id}`
)
: Linking.openURL(
format !== null
? auth.instance +
`/preserved/${link?.id}?format=${format}`
: (link.url as string)
);
}
}}
>
<ContextMenu.ItemTitle>Open Original</ContextMenu.ItemTitle>
</ContextMenu.Item>
{link?.url && (
<>
<ContextMenu.Item
key="copy-url"
onSelect={async () => {
await Clipboard.setStringAsync(link.url as string);
}}
>
<ContextMenu.ItemTitle>Copy URL</ContextMenu.ItemTitle>
</ContextMenu.Item>
</>
)}
<ContextMenu.Item
key="pin-link"
onSelect={async () => {
const isAlreadyPinned =
link?.pinnedBy && link.pinnedBy[0] ? true : false;
await updateLink.mutateAsync({
...link,
pinnedBy: (isAlreadyPinned
? [{ id: undefined }]
: [{ id: user?.id }]) as any,
});
}}
>
<ContextMenu.ItemTitle>
{link.pinnedBy && link.pinnedBy[0] ? "Unpin Link" : "Pin Link"}
</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item
key="edit-link"
onSelect={() => {
SheetManager.show("edit-link-sheet", {
payload: { link: link },
});
}}
>
<ContextMenu.ItemTitle>Edit Link</ContextMenu.ItemTitle>
</ContextMenu.Item>
{link.url && atLeastOneFormatAvailable(link) && (
<ContextMenu.Sub>
<ContextMenu.SubTrigger key="preserved-formats">
<ContextMenu.ItemTitle>Preserved Formats</ContextMenu.ItemTitle>
</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
{formatAvailable(link, "monolith") && (
<ContextMenu.Item
key="preserved-formats-webpage"
onSelect={() =>
router.navigate(
`/links/${link.id}?format=${ArchivedFormat.monolith}`
)
}
>
<ContextMenu.ItemTitle>Webpage</ContextMenu.ItemTitle>
</ContextMenu.Item>
)}
{formatAvailable(link, "image") && (
<ContextMenu.Item
key="preserved-formats-screenshot"
onSelect={() =>
router.navigate(
`/links/${link.id}?format=${
link.image?.endsWith(".png")
? ArchivedFormat.png
: ArchivedFormat.jpeg
}`
)
}
>
<ContextMenu.ItemTitle>Screenshot</ContextMenu.ItemTitle>
</ContextMenu.Item>
)}
{formatAvailable(link, "pdf") && (
<ContextMenu.Item
key="preserved-formats-pdf"
onSelect={() =>
router.navigate(
`/links/${link.id}?format=${ArchivedFormat.pdf}`
)
}
>
<ContextMenu.ItemTitle>PDF</ContextMenu.ItemTitle>
</ContextMenu.Item>
)}
{formatAvailable(link, "readable") && (
<ContextMenu.Item
key="preserved-formats-readable"
onSelect={() =>
router.navigate(
`/links/${link.id}?format=${ArchivedFormat.readability}`
)
}
>
<ContextMenu.ItemTitle>Readable</ContextMenu.ItemTitle>
</ContextMenu.Item>
)}
</ContextMenu.SubContent>
</ContextMenu.Sub>
)}
<ContextMenu.Item
key="delete-link"
onSelect={() => {
return Alert.alert(
"Delete Link",
"Are you sure you want to delete this link? This action cannot be undone.",
[
{
text: "Cancel",
style: "cancel",
},
{
text: "Delete",
style: "destructive",
onPress: () => {
deleteLink.mutate(link.id as number, {
onSuccess: async () => {
await deleteLinkCache(link.id as number);
},
});
},
},
]
);
}}
>
<ContextMenu.ItemTitle>Delete</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
);
};
export default LinkListing;

View File

@@ -0,0 +1,87 @@
import {
View,
FlatList,
Text,
ActivityIndicator,
ViewToken,
} from "react-native";
import LinkListing from "@/components/LinkListing";
import React, { useState } from "react";
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
import Spinner from "@/components/ui/Spinner";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
const RenderItem = React.memo(
({ item }: { item: LinkIncludingShortenedCollectionAndTags }) => {
return <LinkListing link={item} />;
}
);
type Props = {
links: LinkIncludingShortenedCollectionAndTags[];
data: any;
};
export default function Links({ links, data }: Props) {
const { colorScheme } = useColorScheme();
const [promptedRefetch, setPromptedRefetch] = useState(false);
return data.isLoading ? (
<View className="flex justify-center h-screen items-center">
<ActivityIndicator size="large" />
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
</View>
) : (
<FlatList
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={() => <></>}
data={links || []}
refreshControl={
<Spinner
refreshing={data.isRefetching && promptedRefetch}
onRefresh={async () => {
setPromptedRefetch(true);
await data.refetch();
setPromptedRefetch(false);
}}
progressBackgroundColor={
rawTheme[colorScheme as ThemeName]["base-200"]
}
colors={[rawTheme[colorScheme as ThemeName]["base-content"]]}
/>
}
refreshing={data.isRefetching && promptedRefetch}
initialNumToRender={4}
keyExtractor={(item) => item.id?.toString() || ""}
renderItem={({ item }) => (
<RenderItem item={item} key={item.id?.toString()} />
)}
onEndReached={() => data.fetchNextPage()}
onEndReachedThreshold={0.5}
ItemSeparatorComponent={() => (
<View className="bg-neutral-content h-px" />
)}
ListEmptyComponent={
<View className="flex justify-center py-10 items-center">
<Text className="text-center text-xl text-neutral">
Nothing found...
</Text>
</View>
}
onViewableItemsChanged={({
viewableItems,
}: {
viewableItems: ViewToken[];
}) => {
const links = viewableItems.map(
(e) => e.item
) as LinkIncludingShortenedCollectionAndTags[];
if (!data.isRefetching && links.some((e) => e.id && !e.preview))
data.refetch();
}}
viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
/>
);
}

View File

@@ -0,0 +1,120 @@
import { View, Text, Pressable, Platform, Alert } from "react-native";
import { decode } from "html-entities";
import { TagIncludingLinkCount } from "@linkwarden/types";
import useAuthStore from "@/store/auth";
import { useRouter } from "expo-router";
import * as ContextMenu from "zeego/context-menu";
import { cn } from "@linkwarden/lib/utils";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { CalendarDays, Hash, Link } from "lucide-react-native";
import { useRemoveTag } from "@linkwarden/router/tags";
type Props = {
tag: TagIncludingLinkCount;
};
const TagListing = ({ tag }: Props) => {
const { auth } = useAuthStore();
const router = useRouter();
const { colorScheme } = useColorScheme();
const deleteCollection = useRemoveTag(auth);
return (
<ContextMenu.Root>
<ContextMenu.Trigger asChild>
<Pressable
className={cn(
"p-5 flex-row justify-between",
"bg-base-100",
Platform.OS !== "android" && "active:bg-base-200/50"
)}
onLongPress={() => {}}
onPress={() => router.navigate(`/tags/${tag.id}`)}
android_ripple={{
color: colorScheme === "dark" ? "rgba(255,255,255,0.2)" : "#ddd",
borderless: false,
}}
>
<View className="w-full">
<View className="w-[90%] flex-col justify-between gap-3">
<View className="flex flex-row gap-2 items-center pr-1.5 self-start rounded-md">
<Hash
size={16}
color={rawTheme[colorScheme as ThemeName]["primary"]}
/>
<Text
numberOfLines={2}
className="font-medium text-lg text-base-content"
>
{decode(tag.name)}
</Text>
</View>
</View>
<View className="flex-row gap-3">
<View className="flex flex-row gap-1 items-center mt-5 self-start">
<CalendarDays
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
<Text
numberOfLines={1}
className="font-light text-xs text-base-content"
>
{new Date(tag.createdAt).toLocaleString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})}
</Text>
</View>
<View className="flex flex-row gap-1 items-center mt-5 self-start">
<Link
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
<Text
numberOfLines={1}
className="font-light text-xs text-base-content"
>
{tag._count?.links}
</Text>
</View>
</View>
</View>
</Pressable>
</ContextMenu.Trigger>
<ContextMenu.Content avoidCollisions>
<ContextMenu.Item
key="delete-tag"
onSelect={() => {
return Alert.alert(
"Delete Tag",
"Are you sure you want to delete this Tag? This action cannot be undone.",
[
{
text: "Cancel",
style: "cancel",
},
{
text: "Delete",
style: "destructive",
onPress: () => {
deleteCollection.mutate(tag.id as number);
},
},
]
);
}}
>
<ContextMenu.ItemTitle>Delete</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
);
};
export default TagListing;

View File

@@ -0,0 +1,84 @@
import React, { forwardRef } from "react";
import {
TouchableOpacity,
Text,
View,
ActivityIndicator,
type TouchableOpacityProps,
} from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@linkwarden/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-lg disabled:opacity-50 disabled:pointer-events-none",
{
variants: {
variant: {
default: "bg-slate-500 text-white",
primary: "bg-primary text-base-content",
accent: "bg-accent border border-violet-400 text-white",
destructive: "bg-destructive text-white",
outline: "border border-base-content",
secondary: "bg-secondary text-secondary-foreground",
input:
"bg-base-100 rounded-lg px-4 justify-between flex-row font-normal",
metal: "bg-neutral-content text-base-content border border-neutral/30",
ghost: "",
simple: "bg-base-200",
link: "text-primary underline-offset-",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 px-2 py-1 text-xs",
lg: "h-12 px-8",
full: "w-full px-4 py-2",
icon: "h-8 w-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export type ButtonProps = TouchableOpacityProps &
VariantProps<typeof buttonVariants> & {
isLoading?: boolean;
children: React.ReactNode;
className?: string;
};
export const Button = forwardRef<
React.ElementRef<typeof TouchableOpacity>,
ButtonProps
>(
(
{
variant = "default",
size,
className,
isLoading = false,
children,
disabled,
...props
},
ref
) => {
const combinedClasses = cn(buttonVariants({ variant, size }), className);
return (
<TouchableOpacity
ref={ref}
className={combinedClasses}
activeOpacity={0.8}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? <ActivityIndicator /> : children}
</TouchableOpacity>
);
}
);
Button.displayName = "Button";

View File

@@ -0,0 +1,32 @@
import { SymbolView, SymbolViewProps, SymbolWeight } from "expo-symbols";
import { StyleProp, ViewStyle } from "react-native";
export function IconSymbol({
name,
size = 24,
color,
style,
weight = "regular",
}: {
name: SymbolViewProps["name"];
size?: number;
color: string;
style?: StyleProp<ViewStyle>;
weight?: SymbolWeight;
}) {
return (
<SymbolView
weight={weight}
tintColor={color}
resizeMode="scaleAspectFit"
name={name}
style={[
{
width: size,
height: size,
},
style,
]}
/>
);
}

View File

@@ -0,0 +1,50 @@
// This file is a fallback for using MaterialIcons on Android and web.
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { SymbolWeight } from "expo-symbols";
import React from "react";
import { OpaqueColorValue, StyleProp, TextStyle } from "react-native";
// Add your SFSymbol to MaterialIcons mappings here.
const MAPPING = {
// See MaterialIcons here: https://icons.expo.fyi
// See SF Symbols in the SF Symbols app on Mac.
"house.fill": "home",
"paperplane.fill": "send",
"chevron.left.forwardslash.chevron.right": "code",
"chevron.right": "chevron-right",
} as Partial<
Record<
import("expo-symbols").SymbolViewProps["name"],
React.ComponentProps<typeof MaterialIcons>["name"]
>
>;
export type IconSymbolName = keyof typeof MAPPING;
/**
* An icon component that uses native SFSymbols on iOS, and MaterialIcons on Android and web. This ensures a consistent look across platforms, and optimal resource usage.
*
* Icon `name`s are based on SFSymbols and require manual mapping to MaterialIcons.
*/
export function IconSymbol({
name,
size = 24,
color,
style,
}: {
name: IconSymbolName;
size?: number;
color: string | OpaqueColorValue;
style?: StyleProp<TextStyle>;
weight?: SymbolWeight;
}) {
return (
<MaterialIcons
color={color}
size={size}
name={MAPPING[name]}
style={style}
/>
);
}

View File

@@ -0,0 +1,21 @@
import React from "react";
import Svg, { Path, Circle, SvgProps } from "react-native-svg";
export const Chromium = (props: SvgProps) => (
<Svg
width={21}
height={21}
viewBox="0 0 24 24"
fill="none"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<Path d="M10.88 21.94 15.46 14" />
<Path d="M21.17 8H12" />
<Path d="M3.95 6.06 8.54 14" />
<Circle cx={12} cy={12} r={10} />
<Circle cx={12} cy={12} r={4} />
</Svg>
);

View File

@@ -0,0 +1,20 @@
import React, { forwardRef } from "react";
import { TextInput, TextInputProps } from "react-native";
import { cn } from "@linkwarden/lib/utils";
const Input = forwardRef<TextInput, TextInputProps>(
({ className, ...props }, ref) => {
return (
<TextInput
ref={ref}
className={cn(
"bg-base-200 text-base-content rounded-lg px-4 py-2",
className
)}
{...props}
/>
);
}
);
export default Input;

View File

@@ -0,0 +1,10 @@
import React, { forwardRef } from "react";
import { RefreshControl, RefreshControlProps } from "react-native";
const Spinner = forwardRef<RefreshControl, RefreshControlProps>(
(props, ref) => {
return <RefreshControl ref={ref} {...props} />;
}
);
export default Spinner;

View File

@@ -0,0 +1,22 @@
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
import { BlurView } from "expo-blur";
import { StyleSheet } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function BlurTabBarBackground() {
return (
<BlurView
// System chrome material automatically adapts to the system's theme
// and matches the native tab bar appearance on iOS.
tint="systemChromeMaterial"
intensity={100}
style={StyleSheet.absoluteFill}
/>
);
}
export function useBottomTabOverflow() {
const tabHeight = useBottomTabBarHeight();
const { bottom } = useSafeAreaInsets();
return tabHeight - bottom;
}

View File

@@ -0,0 +1,6 @@
// This is a shim for web and Android where the tab bar is generally opaque.
export default undefined;
export function useBottomTabOverflow() {
return 0;
}

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

@@ -0,0 +1,41 @@
{
"cli": {
"version": ">= 16.20.1",
"appVersionSource": "remote"
},
"build": {
"preview": {
"android": {
"buildType": "apk"
}
},
"development": {
"developmentClient": true,
"distribution": "internal"
},
"simulator": {
"extends": "development",
"ios": {
"simulator": true
}
},
"production": {
"corepack": true,
"distribution": "store",
"autoIncrement": true,
"channel": "production"
}
},
"submit": {
"production": {
"ios": {
"ascAppId": "6752550960"
},
"android": {
"serviceAccountKeyPath": "./service-account-file.json",
"track": "internal",
"releaseStatus": "draft"
}
}
}
}

33
apps/mobile/lib/cache.ts Normal file
View File

@@ -0,0 +1,33 @@
import * as FileSystem from "expo-file-system";
export const clearCache = async () => {
await Promise.all([
FileSystem.deleteAsync(FileSystem.documentDirectory + "archivedData", {
idempotent: true,
}),
FileSystem.deleteAsync(FileSystem.documentDirectory + "mmkv", {
idempotent: true,
}),
]);
};
export const deleteLinkCache = async (linkId: number) => {
const readablePath =
FileSystem.documentDirectory + `archivedData/readable/link_${linkId}.html`;
const webpagePath =
FileSystem.documentDirectory + `archivedData/webpage/link_${linkId}.html`;
const jpegPath =
FileSystem.documentDirectory + `archivedData/jpeg/link_${linkId}.jpeg`;
const pngPath =
FileSystem.documentDirectory + `archivedData/png/link_${linkId}.png`;
const pdfPath =
FileSystem.documentDirectory + `archivedData/pdf/link_${linkId}.pdf`;
await Promise.all([
FileSystem.deleteAsync(readablePath, { idempotent: true }),
FileSystem.deleteAsync(webpagePath, { idempotent: true }),
FileSystem.deleteAsync(jpegPath, { idempotent: true }),
FileSystem.deleteAsync(pngPath, { idempotent: true }),
FileSystem.deleteAsync(pdfPath, { idempotent: true }),
]);
};

33
apps/mobile/lib/colors.ts Normal file
View File

@@ -0,0 +1,33 @@
// lib/theme/colors.ts
export type ThemeName = "light" | "dark";
export const rawTheme = {
light: {
primary: "#0369A1",
secondary: "#0891B2",
accent: "#6D28D9",
neutral: "#6B7280",
"neutral-content": "#D1D5DB",
"base-100": "#FFFFFF",
"base-200": "#F3F4F6",
"base-content": "#0A0A0A",
info: "#A5F3FC",
success: "#22C55E",
warning: "#FACC15",
error: "#DC2626",
},
dark: {
primary: "#7DD3FC",
secondary: "#22D3EE",
accent: "#6D28D9",
neutral: "#9CA3AF",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#FAFAFA",
info: "#009EE4",
success: "#00B17D",
warning: "#EAC700",
error: "#F1293C",
},
};

View File

@@ -0,0 +1,14 @@
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 60 * 24,
refetchOnMount: true,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
},
});
export { queryClient };

View File

@@ -0,0 +1,31 @@
import { MMKV } from "react-native-mmkv";
import { Persister } from "@tanstack/react-query-persist-client";
const storage = new MMKV({ id: "react-query" });
export const mmkvPersister: Persister = {
persistClient: async (client) => {
try {
const json = JSON.stringify(client);
storage.set("REACT_QUERY_CACHE", json);
} catch (e) {
console.error("Error persisting client:", e);
}
},
restoreClient: async () => {
try {
const json = storage.getString("REACT_QUERY_CACHE");
return json ? JSON.parse(json) : undefined;
} catch (e) {
console.error("Error restoring client:", e);
return undefined;
}
},
removeClient: async () => {
try {
storage.delete("REACT_QUERY_CACHE");
} catch (e) {
console.error("Error removing client:", e);
}
},
};

23
apps/mobile/lib/theme.ts Normal file
View File

@@ -0,0 +1,23 @@
import { vars } from "nativewind";
import { rawTheme, ThemeName } from "./colors";
const hexToRgb = (hex: string) => {
const [r, g, b] = hex
.replace(/^#/, "")
.match(/.{2}/g)!
.map((h) => parseInt(h, 16));
return `${r} ${g} ${b}`;
};
const makeVars = (scheme: ThemeName) =>
vars(
Object.fromEntries(
Object.entries(rawTheme[scheme]).map(([key, hex]) => [
`--color-${key}`,
hexToRgb(hex),
])
) as Record<string, string>
);
export const lightTheme = makeVars("light");
export const darkTheme = makeVars("dark");

View File

@@ -0,0 +1,6 @@
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: "./styles/global.css" });

3
apps/mobile/nativewind-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.

87
apps/mobile/package.json Normal file
View File

@@ -0,0 +1,87 @@
{
"name": "@linkwarden/mobile",
"main": "expo-router/entry",
"version": "0.0.0",
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "expo lint"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"@expo/vector-icons": "^14.0.2",
"@linkwarden/lib": "*",
"@linkwarden/prisma": "*",
"@linkwarden/react-native-render-html": "^6.3.4",
"@linkwarden/router": "*",
"@linkwarden/types": "*",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-menu/menu": "1.2.2",
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@tanstack/react-query": "^5.51.15",
"@tanstack/react-query-persist-client": "^5.51.15",
"class-variance-authority": "^0.7.1",
"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",
"expo-file-system": "~18.0.12",
"expo-font": "~13.0.1",
"expo-haptics": "~14.0.0",
"expo-linking": "~7.0.5",
"expo-router": "4.0.20",
"expo-secure-store": "~14.0.0",
"expo-share-intent": "^3.2.3",
"expo-splash-screen": "~0.29.18",
"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",
"nativewind": "^4.1.23",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.9",
"react-native-actions-sheet": "^0.9.7",
"react-native-blob-util": "^0.23.2",
"react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "~2.20.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-pdf": "^7.0.3",
"react-native-reanimated": "3.16.2",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.1.0",
"react-native-svg": "^15.12.1",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"tailwindcss": "^3.4.17",
"zeego": "^3.0.6",
"zustand": "^5.0.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/jest": "^29.5.12",
"@types/react": "18.3.1",
"@types/react-test-renderer": "^18.3.0",
"jest": "^29.2.1",
"jest-expo": "~52.0.2",
"react-test-renderer": "18.3.1",
"typescript": "^5.8.3"
},
"private": true
}

View File

@@ -0,0 +1,43 @@
const { withAndroidStyles } = require("@expo/config-plugins");
function mutateStylesXml(xml) {
const styles = xml.resources?.style ?? [];
let appTheme = styles.find((s) => s.$?.name === "AppTheme");
if (!appTheme) {
appTheme = {
$: { name: "AppTheme", parent: "Theme.AppCompat.DayNight.NoActionBar" },
item: [],
};
styles.push(appTheme);
}
appTheme.$ = appTheme.$ || {};
appTheme.$.parent = "Theme.AppCompat.DayNight.NoActionBar";
appTheme.item = appTheme.item ?? [];
appTheme.item = appTheme.item.filter(
(i) => i?.$?.name !== "android:textColor"
);
const navItem = appTheme.item.find(
(i) => i.$?.name === "android:navigationBarColor"
);
if (navItem) {
navItem._ = "@android:color/transparent";
} else {
appTheme.item.push({
$: { name: "android:navigationBarColor" },
_: "@android:color/transparent",
});
}
xml.resources.style = styles;
return xml;
}
module.exports = function withDayNightTransparentNav(config) {
return withAndroidStyles(config, (c) => {
c.modResults = mutateStylesXml(c.modResults);
return c;
});
};

138
apps/mobile/store/auth.ts Normal file
View File

@@ -0,0 +1,138 @@
import { create } from "zustand";
import * as SecureStore from "expo-secure-store";
import { router } from "expo-router";
import { MobileAuth } from "@linkwarden/types";
import { Alert } from "react-native";
import { queryClient } from "@/lib/queryClient";
import { mmkvPersister } from "@/lib/queryPersister";
import { clearCache } from "@/lib/cache";
type AuthStore = {
auth: MobileAuth;
signIn: (
username: string,
password: string,
instance: string,
token?: string
) => Promise<void>;
signOut: () => Promise<void>;
setAuth: () => Promise<void>;
};
const useAuthStore = create<AuthStore>((set) => ({
auth: {
instance: "",
session: null,
status: "loading" as const,
},
setAuth: async () => {
const session = await SecureStore.getItemAsync("TOKEN");
const instance = await SecureStore.getItemAsync("INSTANCE");
if (session) {
set({
auth: {
instance,
session,
status: "authenticated",
},
});
} else {
set({
auth: {
instance: instance || "https://cloud.linkwarden.app",
session: null,
status: "unauthenticated",
},
});
}
},
signIn: async (username, password, instance, token) => {
if (process.env.EXPO_PUBLIC_SHOW_LOGS === "true")
console.log("Signing into", instance);
if (token) {
// make a request to the API to validate the token
await fetch(instance + "/api/v1/users/me", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
}).then(async (res) => {
if (res.ok) {
await SecureStore.setItemAsync("INSTANCE", instance);
await SecureStore.setItemAsync("TOKEN", token);
set({
auth: {
session: token,
instance,
status: "authenticated",
},
});
router.replace("/(tabs)/dashboard");
} else {
Alert.alert("Error", "Invalid token");
}
});
} else {
try {
const res = await Promise.race([
fetch(`${instance}/api/v1/session`, {
method: "POST",
body: JSON.stringify({ username, password }),
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" } });
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 () => {
await SecureStore.deleteItemAsync("TOKEN");
await SecureStore.deleteItemAsync("INSTANCE");
queryClient.cancelQueries();
queryClient.clear();
mmkvPersister.removeClient?.();
await clearCache();
set({
auth: {
instance: "",
session: null,
status: "unauthenticated",
},
});
router.replace("/");
},
}));
export default useAuthStore;

37
apps/mobile/store/data.ts Normal file
View File

@@ -0,0 +1,37 @@
import { create } from "zustand";
import { MobileData } from "@linkwarden/types";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { colorScheme } from "nativewind";
type DataStore = {
data: MobileData;
updateData: (newData: Partial<MobileData>) => void;
setData: () => void;
};
const useDataStore = create<DataStore>((set, get) => ({
data: {
shareIntent: {
hasShareIntent: false,
url: "",
},
theme: "system",
preferredBrowser: "app",
},
setData: async () => {
const dataString = JSON.parse((await AsyncStorage.getItem("data")) || "{}");
colorScheme.set(dataString.theme || "system");
if (dataString)
set((state) => ({ data: { ...state.data, ...dataString } }));
},
updateData: async (patch) => {
const merged = { ...get().data, ...patch };
const { shareIntent, ...persistable } = merged;
await AsyncStorage.setItem("data", JSON.stringify(persistable));
set({ data: merged });
},
}));
export default useDataStore;

26
apps/mobile/store/tmp.ts Normal file
View File

@@ -0,0 +1,26 @@
import { create } from "zustand";
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
import { User } from "@linkwarden/prisma/client";
type Tmp = {
link: LinkIncludingShortenedCollectionAndTags | null;
user: Pick<User, "id"> | null;
};
type TmpStore = {
tmp: Tmp;
updateTmp: (newData: Partial<Tmp>) => void;
};
const useTmpStore = create<TmpStore>((set, get) => ({
tmp: {
link: null,
user: null,
},
updateTmp: async (patch) => {
const merged = { ...get().tmp, ...patch };
set({ tmp: merged });
},
}));
export default useTmpStore;

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,39 @@
/** @type {import('tailwindcss').Config} */
const { rawTheme } = require("./lib/colors");
const hexToRgb = (hex) => {
const [r, g, b] = hex
.replace(/^#/, "")
.match(/.{2}/g)
.map((h) => parseInt(h, 16));
return `${r} ${g} ${b}`;
};
module.exports = {
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
darkMode: "media",
theme: {
extend: {
colors: Object.fromEntries(
Object.keys(rawTheme.light).map((key) => [
key,
`rgb(var(--color-${key}) / <alpha-value>)`,
])
),
},
},
plugins: [
({ addBase }) => {
addBase({
":root": Object.fromEntries(
Object.entries(rawTheme.light).map(([key, hex]) => [
`--color-${key}`,
hexToRgb(hex),
])
),
});
},
],
};

18
apps/mobile/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts",
"nativewind-env.d.ts"
]
}

View File

@@ -0,0 +1,43 @@
export enum Sort {
DateNewestFirst,
DateOldestFirst,
NameAZ,
NameZA,
DescriptionAZ,
DescriptionZA,
}
export type LinkRequestQuery = {
sort?: Sort;
cursor?: number;
collectionId?: number;
tagId?: number;
pinnedOnly?: boolean;
searchQueryString?: string;
searchByName?: boolean;
searchByUrl?: boolean;
searchByDescription?: boolean;
searchByTextContent?: boolean;
searchByTags?: boolean;
};
export type LinkIncludingShortenedCollectionAndTags = {
id: number;
name: string;
url: string;
description: string;
type: "url" | "image" | "pdf";
preview: string | null;
createdAt: string;
updatedAt: string;
collectionId: number;
tags: { id: number; name: string }[];
};
export enum ArchivedFormat {
png = 0,
jpeg = 1,
pdf = 2,
readability = 3,
monolith = 4,
}

1
apps/mobile/types/nativewind-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="nativewind/types" />

21
apps/web/components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,52 @@
import Link from "next/link";
import React, { MouseEventHandler } from "react";
import { Trans } from "next-i18next";
import { Button } from "@/components/ui/button";
type Props = {
toggleAnnouncementBar: MouseEventHandler<HTMLButtonElement>;
};
export default function Announcement({ toggleAnnouncementBar }: Props) {
const announcementId = localStorage.getItem("announcementId");
const announcementMessage = localStorage.getItem("announcementMessage");
return (
<div className="fixed mx-auto bottom-20 sm:bottom-10 w-full pointer-events-none p-5 z-30">
<div className="mx-auto pointer-events-auto p-2 flex justify-between gap-2 items-center border border-primary shadow-xl rounded-xl bg-base-300 backdrop-blur-sm bg-opacity-80 max-w-md">
<i className="bi-stars text-xl text-yellow-600 dark:text-yellow-500"></i>
<p className="w-4/5 text-center text-sm sm:text-base">
{announcementId ? (
<Trans
i18nKey="new_version_announcement"
values={{ version: announcementId }}
components={[
<Link
href={`https://linkwarden.app/blog/releases/${announcementId}`}
target="_blank"
className="underline decoration-dotted underline-offset-4 hover:text-primary duration-100"
key={0}
/>,
]}
/>
) : announcementMessage ? (
<Trans
i18nKey={announcementMessage}
components={[
<Link
href={`https://linkwarden.app/blog`}
target="_blank"
className="underline decoration-dotted underline-offset-4 hover:text-primary duration-100"
key={0}
/>,
]}
/>
) : undefined}
</p>
<Button variant="ghost" size="icon" onClick={toggleAnnouncementBar}>
<i className="bi-x text-xl"></i>
</Button>
</div>
</div>
);
}

View File

@@ -1,8 +1,8 @@
import useLocalSettingsStore from "@/store/localSettings";
import Image from "next/image";
import Link from "next/link";
import React, { ReactNode } from "react";
import { Trans } from "next-i18next";
import { useUser } from "@linkwarden/router/user";
interface Props {
text?: string;
@@ -15,7 +15,7 @@ export default function CenteredForm({
children,
"data-testid": dataTestId,
}: Props) {
const { settings } = useLocalSettingsStore();
const { data: user } = useUser();
return (
<div
@@ -23,29 +23,39 @@ export default function CenteredForm({
data-testid={dataTestId}
>
<div className="m-auto flex flex-col gap-2 w-full">
{settings.theme ? (
{user?.theme === "light" ? (
<Image
src={`/linkwarden_${
settings.theme === "dark" ? "dark" : "light"
}.png`}
src={"/linkwarden_light.png"}
width={640}
height={136}
alt="Linkwarden"
className="h-12 w-fit mx-auto"
className="h-12 w-auto mx-auto"
/>
) : undefined}
{text ? (
) : (
<Image
src={"/linkwarden_dark.png"}
width={640}
height={136}
alt="Linkwarden"
className="h-12 w-auto mx-auto"
/>
)}
{text && (
<p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold px-2 text-center">
{text}
</p>
) : undefined}
)}
{children}
<p className="text-center text-xs text-neutral mb-5">
<Trans
values={{ date: new Date().getFullYear() }}
i18nKey="all_rights_reserved"
components={[
<Link href="https://linkwarden.app" className="font-semibold" />,
<Link
href="https://linkwarden.app"
className="font-semibold"
key="linkwarden-website-key"
/>,
]}
/>
</p>

View File

@@ -5,20 +5,29 @@ type Props = {
state: boolean;
className?: string;
onClick: ChangeEventHandler<HTMLInputElement>;
disabled?: boolean;
};
export default function Checkbox({ label, state, className, onClick }: Props) {
export default function Checkbox({
label,
state,
className,
onClick,
disabled,
}: Props) {
return (
<label
className={`label cursor-pointer flex gap-2 justify-start ${
className || ""
}`}
aria-disabled={disabled}
>
<input
type="checkbox"
checked={state}
onChange={onClick}
className="checkbox checkbox-primary"
disabled={disabled}
/>
<span className="label-text">{label}</span>
</label>

View File

@@ -31,6 +31,11 @@ function useOutsideAlerter(
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
const clickedElement = event.target as HTMLElement;
if (clickedElement.closest("[data-ignore-click-away]")) {
return;
}
if (ref.current && !ref.current.contains(clickedElement)) {
const refZIndex = getZIndex(ref.current);
const clickedZIndex = getZIndex(clickedElement);

View File

@@ -1,29 +1,36 @@
import Link from "next/link";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import {
AccountSettings,
CollectionIncludingMembersAndLinkCount,
} from "@linkwarden/types";
import React, { useEffect, useState } from "react";
import ProfilePhoto from "./ProfilePhoto";
import usePermissions from "@/hooks/usePermissions";
import useLocalSettingsStore from "@/store/localSettings";
import getPublicUserData from "@/lib/client/getPublicUserData";
import useAccountStore from "@/store/account";
import EditCollectionModal from "./ModalContent/EditCollectionModal";
import EditCollectionSharingModal from "./ModalContent/EditCollectionSharingModal";
import DeleteCollectionModal from "./ModalContent/DeleteCollectionModal";
import { dropdownTriggerer } from "@/lib/client/utils";
import { useTranslation } from "next-i18next";
import { useUser } from "@linkwarden/router/user";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { Button } from "./ui/button";
type Props = {
export default function CollectionCard({
collection,
}: {
collection: CollectionIncludingMembersAndLinkCount;
className?: string;
};
export default function CollectionCard({ collection, className }: Props) {
}) {
const { t } = useTranslation();
const { settings } = useLocalSettingsStore();
const { account } = useAccountStore();
const { data: user } = useUser();
const formattedDate = new Date(collection.createdAt as string).toLocaleString(
"en-US",
t("locale"),
{
year: "numeric",
month: "short",
@@ -33,30 +40,24 @@ export default function CollectionCard({ collection, className }: Props) {
const permissions = usePermissions(collection.id as number);
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => {
const fetchOwner = async () => {
if (collection && collection.ownerId !== account.id) {
if (collection && collection.ownerId !== user?.id) {
const owner = await getPublicUserData(collection.ownerId as number);
setCollectionOwner(owner);
} else if (collection && collection.ownerId === account.id) {
} else if (collection && collection.ownerId === user?.id) {
setCollectionOwner({
id: account.id as number,
name: account.name,
username: account.username as string,
image: account.image as string,
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
archiveAsMonolith: account.archiveAsMonolith as boolean,
archiveAsPDF: account.archiveAsPDF as boolean,
id: user?.id as number,
name: user?.name,
username: user?.username as string,
image: user?.image as string,
archiveAsScreenshot: user?.archiveAsScreenshot as boolean,
archiveAsMonolith: user?.archiveAsMonolith as boolean,
archiveAsPDF: user?.archiveAsPDF as boolean,
});
}
};
@@ -71,70 +72,68 @@ export default function CollectionCard({ collection, className }: Props) {
return (
<div className="relative">
<div className="dropdown dropdown-bottom dropdown-end absolute top-3 right-3 z-20">
<div
tabIndex={0}
role="button"
onMouseDown={dropdownTriggerer}
className="btn btn-ghost btn-sm btn-square text-neutral"
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="absolute top-3 right-3 z-20"
>
<i title="More" className="bi-three-dots text-xl text-neutral" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
sideOffset={4}
side="bottom"
align="end"
className="z-[30]"
>
<i className="bi-three-dots text-xl" title="More"></i>
</div>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
{permissions === true && (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditCollectionModal(true);
}}
>
{t("edit_collection_info")}
</div>
</li>
<DropdownMenuItem onSelect={() => setEditCollectionModal(true)}>
<i className="bi-pencil-square" />
{t("edit_collection_info")}
</DropdownMenuItem>
)}
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditCollectionSharingModal(true);
}}
>
{permissions === true
? t("share_and_collaborate")
: t("view_team")}
</div>
</li>
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setDeleteCollectionModal(true);
}}
>
{permissions === true
? t("delete_collection")
: t("leave_collection")}
</div>
</li>
</ul>
</div>
<DropdownMenuItem
onSelect={() => setEditCollectionSharingModal(true)}
>
<i className="bi-globe" />
{permissions === true ? t("share_and_collaborate") : t("view_team")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => setDeleteCollectionModal(true)}
className="text-error"
>
{permissions === true ? (
<>
<i className="bi-trash" />
{t("delete_collection")}
</>
) : (
<>
<i className="bi-box-arrow-left" />
{t("leave_collection")}
</>
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div
className="flex items-center absolute bottom-3 left-3 z-10 btn px-2 btn-ghost rounded-full"
className="flex items-center absolute bottom-3 left-3 z-10 px-1 py-1 rounded-full cursor-pointer hover:bg-base-content/20 transition-colors duration-200"
onClick={() => setEditCollectionSharingModal(true)}
>
{collectionOwner.id ? (
{collectionOwner.id && (
<ProfilePhoto
src={collectionOwner.image || undefined}
name={collectionOwner.name}
/>
) : undefined}
)}
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
@@ -148,21 +147,21 @@ export default function CollectionCard({ collection, className }: Props) {
);
})
.slice(0, 3)}
{collection.members.length - 3 > 0 ? (
{collection.members.length - 3 > 0 && (
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{collection.members.length - 3}</span>
</div>
</div>
) : null}
)}
</div>
<Link
href={`/collections/${collection.id}`}
style={{
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
user?.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
} 50%, ${
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
user?.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
} 100%)`,
}}
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content"
@@ -178,15 +177,15 @@ export default function CollectionCard({ collection, className }: Props) {
<div className="flex justify-end items-center">
<div className="text-right">
<div className="font-bold text-sm flex justify-end gap-1 items-center">
{collection.isPublic ? (
{collection.isPublic && (
<i
className="bi-globe2 drop-shadow text-neutral"
title="This collection is being shared publicly."
title={t("collection_publicly_shared")}
></i>
) : undefined}
)}
<i
className="bi-link-45deg text-lg text-neutral"
title="This collection is being shared publicly."
title={t("links")}
></i>
{collection._count && collection._count.links}
</div>
@@ -194,7 +193,7 @@ export default function CollectionCard({ collection, className }: Props) {
<p className="font-bold text-xs flex gap-1 items-center">
<i
className="bi-calendar3 text-neutral"
title="This collection is being shared publicly."
title={t("collection_publicly_shared")}
></i>
{formattedDate}
</p>
@@ -203,24 +202,24 @@ export default function CollectionCard({ collection, className }: Props) {
</div>
</div>
</Link>
{editCollectionModal ? (
{editCollectionModal && (
<EditCollectionModal
onClose={() => setEditCollectionModal(false)}
activeCollection={collection}
/>
) : undefined}
{editCollectionSharingModal ? (
)}
{editCollectionSharingModal && (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
) : undefined}
{deleteCollectionModal ? (
)}
{deleteCollectionModal && (
<DeleteCollectionModal
onClose={() => setDeleteCollectionModal(false)}
activeCollection={collection}
/>
) : undefined}
)}
</div>
);
}

View File

@@ -0,0 +1,497 @@
import React, { useEffect, useMemo, useState } from "react";
import Tree, {
mutateTree,
moveItemOnTree,
RenderItemParams,
TreeItem,
TreeData,
ItemId,
TreeSourcePosition,
TreeDestinationPosition,
} from "@atlaskit/tree";
import { Collection } from "@linkwarden/prisma/client";
import Link from "next/link";
import { CollectionIncludingMembersAndLinkCount } from "@linkwarden/types";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import { useTranslation } from "next-i18next";
import {
useCollections,
useUpdateCollection,
} from "@linkwarden/router/collections";
import { useUpdateUser, useUser } from "@linkwarden/router/user";
import Icon from "./Icon";
import { IconWeight } from "@phosphor-icons/react";
import Droppable from "./Droppable";
import { cn } from "@linkwarden/lib";
import { Active, useDndContext } from "@dnd-kit/core";
interface ExtendedTreeItem extends TreeItem {
data: Collection;
}
const CollectionListing = () => {
const { active: droppableActive } = useDndContext();
const { t } = useTranslation();
const updateCollection = useUpdateCollection();
const { data: collections = [], isLoading } = useCollections();
const { data: user } = useUser();
const updateUser = useUpdateUser();
const router = useRouter();
const [tree, setTree] = useState<TreeData | undefined>();
const initialTree = useMemo(() => {
if (collections.length > 0) {
return buildTreeFromCollections(
collections,
router,
tree,
user?.collectionOrder
);
} else return undefined;
}, [collections, user]);
useEffect(() => {
setTree(initialTree);
}, [initialTree]);
useEffect(() => {
if (user?.username) {
// refetch();
if (
(!user.collectionOrder || user.collectionOrder.length === 0) &&
collections.length > 0
)
updateUser.mutate({
...user,
collectionOrder: collections
.filter((e) => e.parentId === null)
.map((e) => e.id as number),
});
else {
const newCollectionOrder: number[] = [...(user.collectionOrder || [])];
// Start with collections that are in both account.collectionOrder and collections
const existingCollectionIds = collections.map((c) => c.id as number);
const filteredCollectionOrder = user.collectionOrder.filter((id: any) =>
existingCollectionIds.includes(id)
);
// Add new collections that are not in account.collectionOrder and meet the specific conditions
collections.forEach((collection) => {
if (
!filteredCollectionOrder.includes(collection.id as number) &&
(!collection.parentId || collection.ownerId === user.id)
) {
filteredCollectionOrder.push(collection.id as number);
}
});
// check if the newCollectionOrder is the same as the old one
if (
JSON.stringify(newCollectionOrder) !==
JSON.stringify(user.collectionOrder)
) {
updateUser.mutateAsync({
...user,
collectionOrder: newCollectionOrder,
});
}
}
}
}, [user, collections]);
const onExpand = (movedCollectionId: ItemId) => {
setTree((currentTree) =>
mutateTree(currentTree!, movedCollectionId, { isExpanded: true })
);
};
const onCollapse = (movedCollectionId: ItemId) => {
setTree((currentTree) =>
mutateTree(currentTree as TreeData, movedCollectionId, {
isExpanded: false,
})
);
};
function reorderTreeItems(
tree: TreeData,
movedCollectionId: ItemId,
source: TreeSourcePosition,
destination: TreeDestinationPosition
) {
// Same parent reordering
if (source.parentId === destination.parentId) {
const parent = tree.items[source.parentId];
const children = [...parent.children];
// Remove from source index
children.splice(source.index, 1);
// Insert at destination index
if (destination.index !== undefined) {
children.splice(destination.index, 0, movedCollectionId);
}
parent.children = children;
return tree;
}
// Different parent move
const sourceParent = tree.items[source.parentId];
const destinationParent = tree.items[destination.parentId];
// Remove from source parent
sourceParent.children = sourceParent.children.filter(
(id) => id !== movedCollectionId
);
// Initialize children array if it doesn't exist
if (!destinationParent.children) {
destinationParent.children = [];
}
// If destination index is not specified, add to the end
const destinationIndex =
destination.index !== undefined
? destination.index
: destinationParent.children.length;
// Add to destination parent
destinationParent.children.splice(destinationIndex, 0, movedCollectionId);
// Update destination parent properties
destinationParent.hasChildren = true;
destinationParent.isExpanded = true;
// Update the moved item's parent ID
tree.items[movedCollectionId].data.parentId = destination.parentId;
return tree;
}
function flattenTreeIds(
tree: TreeData,
nodeId: ItemId = "root",
result: Array<ItemId> = []
) {
const node = tree.items[nodeId];
if (nodeId !== "root") {
result.push(node.id);
}
if (node.children && node.children.length > 0) {
node.children.forEach((childId) => {
flattenTreeIds(tree, childId, result);
});
}
return result;
}
const onDragEnd = async (
source: TreeSourcePosition,
destination: TreeDestinationPosition | undefined
) => {
if (!destination || !tree) {
return;
}
if (
source.index === destination.index &&
source.parentId === destination.parentId
) {
return;
}
const movedCollectionId = Number(
tree.items[source.parentId].children[source.index]
);
const movedCollection = collections.find((c) => c.id === movedCollectionId);
const destinationCollection = collections.find(
(c) => c.id === Number(destination.parentId)
);
if (
(movedCollection?.ownerId !== user?.id &&
destination.parentId !== source.parentId) ||
(destinationCollection?.ownerId !== user?.id &&
destination.parentId !== "root")
) {
return toast.error(t("cant_change_collection_you_dont_own"));
}
setTree((currentTree) => moveItemOnTree(currentTree!, source, destination));
const newTree = reorderTreeItems(
tree,
movedCollectionId,
source,
destination
);
if (source.parentId !== destination.parentId) {
await updateCollection.mutateAsync(
{
...movedCollection,
parentId:
destination.parentId && destination.parentId !== "root"
? Number(destination.parentId)
: destination.parentId === "root"
? "root"
: null,
},
{
onError: (error) => {
toast.error(error.message);
},
}
);
}
await updateUser.mutateAsync({
...user,
collectionOrder: flattenTreeIds(newTree),
});
};
if (isLoading) {
return (
<div className="flex flex-col gap-4">
<div className="skeleton h-4 w-full"></div>
<div className="skeleton h-4 w-full"></div>
<div className="skeleton h-4 w-full"></div>
</div>
);
} else if (!tree) {
return (
<p className="text-neutral text-xs font-semibold truncate w-full px-2 mt-5 mb-8">
{t("you_have_no_collections")}
</p>
);
} else
return (
<Tree
tree={tree}
renderItem={(itemProps) =>
renderItem({ ...itemProps }, router.asPath, droppableActive)
}
onExpand={onExpand}
onCollapse={onCollapse}
onDragEnd={onDragEnd}
isDragEnabled
isNestingEnabled
/>
);
};
export default CollectionListing;
const renderItem = (
{ item, onExpand, onCollapse, provided }: RenderItemParams,
currentPath: string,
droppableActive: Active | null
) => {
const collection = item.data;
return (
<Droppable
id={`side-bar-collection-${collection.id}`}
data={{
name: collection.name,
id: collection.id,
ownerId: collection.ownerId,
}}
className="group"
>
<div
ref={provided.innerRef}
{...provided.draggableProps}
className="mb-1"
>
<div
className={cn(
currentPath === `/collections/${collection.id}`
? "bg-primary/20 is-active"
: droppableActive
? "select-none"
: "hover:bg-neutral/20",
"duration-100 flex gap-1 items-center pr-2 pl-1 rounded-md"
)}
>
{Dropdown(item as ExtendedTreeItem, onExpand, onCollapse)}
<Link
href={`/collections/${collection.id}`}
className="w-full"
{...provided.dragHandleProps}
>
<div
className={`py-1 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
>
{collection.icon ? (
<Icon
icon={collection.icon}
size={30}
weight={(collection.iconWeight || "regular") as IconWeight}
color={collection.color}
className="-mr-[0.15rem]"
/>
) : (
<i
className="bi-folder-fill text-xl"
style={{ color: collection.color }}
></i>
)}
<p className="truncate w-full">{collection.name}</p>
{collection.isPublic && (
<i
className="bi-globe2 text-sm text-black/50 dark:text-white/50 drop-shadow"
title="This collection is being shared publicly."
></i>
)}
<div className="drop-shadow text-neutral text-xs">
{collection._count?.links}
</div>
</div>
</Link>
</div>
</div>
</Droppable>
);
};
const Dropdown = (
item: ExtendedTreeItem,
onExpand: (id: ItemId) => void,
onCollapse: (id: ItemId) => void
) => {
if (item.children && item.children.length > 0) {
return item.isExpanded ? (
<button onClick={() => onCollapse(item.id)}>
<div className="bi-caret-down-fill opacity-50 hover:opacity-100 duration-200"></div>
</button>
) : (
<button onClick={() => onExpand(item.id)}>
<div className="bi-caret-right-fill opacity-40 hover:opacity-100 duration-200"></div>
</button>
);
}
// return <span>&bull;</span>;
return <div></div>;
};
const buildTreeFromCollections = (
collections: CollectionIncludingMembersAndLinkCount[],
router: ReturnType<typeof useRouter>,
tree?: TreeData,
order?: number[]
): TreeData => {
if (order) {
collections.sort((a: any, b: any) => {
return order.indexOf(a.id) - order.indexOf(b.id);
});
}
function getTotalLinkCount(collectionId: number): number {
const collection = items[collectionId];
if (!collection) {
return 0;
}
let totalLinkCount = (collection.data as any)._count?.links || 0;
if (collection.hasChildren) {
collection.children.forEach((childId) => {
totalLinkCount += getTotalLinkCount(childId as number);
});
}
return totalLinkCount;
}
const items: { [key: string]: ExtendedTreeItem } = collections.reduce(
(acc: any, collection) => {
acc[collection.id as number] = {
id: collection.id,
children: [],
hasChildren: false,
isExpanded: tree?.items[collection.id as number]?.isExpanded || false,
data: {
id: collection.id,
parentId: collection.parentId,
name: collection.name,
description: collection.description,
color: collection.color,
icon: collection.icon,
iconWeight: collection.iconWeight,
isPublic: collection.isPublic,
ownerId: collection.ownerId,
createdAt: collection.createdAt,
updatedAt: collection.updatedAt,
_count: {
links: collection._count?.links,
},
},
};
return acc;
},
{}
);
const activeCollectionId = Number(router.asPath.split("/collections/")[1]);
if (activeCollectionId) {
for (const item in items) {
const collection = items[item];
if (Number(item) === activeCollectionId && collection.data.parentId) {
// get all the parents of the active collection recursively until root and set isExpanded to true
let parentId = collection.data.parentId || null;
while (parentId && items[parentId]) {
items[parentId].isExpanded = true;
parentId = items[parentId].data.parentId;
}
}
}
}
collections.forEach((collection) => {
const parentId = collection.parentId;
if (parentId && items[parentId] && collection.id) {
items[parentId].children.push(collection.id);
items[parentId].hasChildren = true;
}
});
collections.forEach((collection) => {
const collectionId = collection.id;
if (items[collectionId as number] && collection.id) {
const linkCount = getTotalLinkCount(collectionId as number);
(items[collectionId as number].data as any)._count.links = linkCount;
}
});
const rootId = "root";
items[rootId] = {
id: rootId,
children: (collections
.filter(
(c) =>
c.parentId === null || !collections.find((i) => i.id === c.parentId)
)
.map((c) => c.id) || "") as unknown as string[],
hasChildren: true,
isExpanded: true,
data: { name: "Root" } as Collection,
};
return { rootId, items };
};

View File

@@ -0,0 +1,50 @@
import React, { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { useTranslation } from "next-i18next";
import Modal from "./Modal";
import { Separator } from "./ui/separator";
type Props = {
toggleModal: Function;
className?: string;
children: ReactNode;
title: string;
onConfirmed: Function;
dismissible?: boolean;
};
export default function ConfirmationModal({
toggleModal,
className,
children,
title,
onConfirmed,
}: Props) {
const { t } = useTranslation();
return (
<Modal toggleModal={toggleModal} className={className}>
<p className="text-xl font-thin">{title}</p>
<Separator className="mb-3 mt-1" />
{children}
<div className="w-full flex items-center justify-end gap-2 mt-3">
<Button
variant="ghost"
className="hover:bg-base-200"
onClick={() => toggleModal()}
>
{t("cancel")}
</Button>
<Button
variant="destructive"
onClick={async () => {
await onConfirmed();
toggleModal();
}}
>
{t("confirm")}
</Button>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,60 @@
import React, { useState } from "react";
import { Button } from "./ui/button";
type Props = {
text: string;
};
const CopyButton: React.FC<Props> = ({ text }) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1000);
} catch (err) {
console.log(err);
}
};
return (
<Button variant="ghost" type="button" size="icon" onClick={handleCopy}>
{copied ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="h-5 w-5 text-success"
viewBox="0 0 16 16"
>
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="h-5 w-5"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2
2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1
1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0
0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0
0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2
2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z"
/>
</svg>
)}
</Button>
);
};
export default CopyButton;

View File

@@ -0,0 +1,23 @@
export default function dashboardItem({
name,
value,
icon,
}: {
name: string;
value: number;
icon: string;
}) {
return (
<div className="flex items-center justify-between w-full rounded-xl border border-neutral-content p-3 bg-gradient-to-tr from-neutral-content/70 to-50% to-base-200">
<div className="w-14 aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
<i className={`${icon} text-primary text-3xl drop-shadow`}></i>
</div>
<div className="ml-4 flex flex-col justify-center">
<p className="text-neutral text-xs tracking-wider text-right">{name}</p>
<p className="font-thin text-4xl text-primary mt-0.5 text-right">
{value || 0}
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,411 @@
import React, { useState, useMemo, useEffect } from "react";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
} from "@/components/ui/dropdown-menu";
import { useTranslation } from "next-i18next";
import { Button } from "@/components/ui/button";
import TextInput from "./TextInput";
import { useCollections } from "@linkwarden/router/collections";
import {
DashboardSection,
DashboardSectionType,
} from "@linkwarden/prisma/client";
import { useUser } from "@linkwarden/router/user";
import { useUpdateDashboardLayout } from "@linkwarden/router/dashboardData";
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
DndContext,
DragEndEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { restrictToParentElement } from "@dnd-kit/modifiers";
import { cn } from "@linkwarden/lib";
import toast from "react-hot-toast";
interface DashboardSectionOption {
type: DashboardSectionType;
name: string;
collectionId?: number;
enabled: boolean;
order?: number;
}
export default function DashboardLayoutDropdown() {
const { t } = useTranslation();
const { data: user } = useUser();
const { data: collections = [] } = useCollections();
const updateDashboardLayout = useUpdateDashboardLayout();
const [searchTerm, setSearchTerm] = useState("");
const mouseSensor = useSensor(MouseSensor, {
// Require the mouse to move by 10 pixels before activating
activationConstraint: {
distance: 10,
},
});
const touchSensor = useSensor(TouchSensor, {
// Press delay of 200ms, with tolerance of 5px of movement
activationConstraint: {
delay: 200,
tolerance: 5,
},
});
const sensors = useSensors(mouseSensor, touchSensor);
const [dashboardSections, setDashboardSections] = useState<
DashboardSection[]
>(user?.dashboardSections || []);
useEffect(() => {
setDashboardSections(user?.dashboardSections || []);
}, [user?.dashboardSections]);
const getSectionOrder = (
type: DashboardSectionType,
collectionId?: number
): number | undefined => {
const section = dashboardSections.find(
(section) =>
section.type === type &&
(type === DashboardSectionType.COLLECTION
? section.collectionId === collectionId
: true)
);
return section?.order;
};
const isSectionEnabled = (
type: DashboardSectionType,
collectionId?: number
): boolean => {
return dashboardSections.some(
(section) =>
section.type === type &&
(type === DashboardSectionType.COLLECTION
? section.collectionId === collectionId
: true)
);
};
const defaultSections: DashboardSectionOption[] = useMemo(
() => [
{
type: DashboardSectionType.STATS,
name: t("dashboard_stats"),
enabled: isSectionEnabled(DashboardSectionType.STATS),
order: getSectionOrder(DashboardSectionType.STATS),
},
{
type: DashboardSectionType.RECENT_LINKS,
name: t("recent_links"),
enabled: isSectionEnabled(DashboardSectionType.RECENT_LINKS),
order: getSectionOrder(DashboardSectionType.RECENT_LINKS),
},
{
type: DashboardSectionType.PINNED_LINKS,
name: t("pinned_links"),
enabled: isSectionEnabled(DashboardSectionType.PINNED_LINKS),
order: getSectionOrder(DashboardSectionType.PINNED_LINKS),
},
],
[dashboardSections]
);
const collectionSections = useMemo(
() =>
collections.map((collection) => ({
type: DashboardSectionType.COLLECTION,
name: collection.name,
collectionId: collection.id,
enabled: isSectionEnabled(
DashboardSectionType.COLLECTION,
collection.id
),
order: getSectionOrder(DashboardSectionType.COLLECTION, collection.id),
})),
[collections, dashboardSections]
);
const allSections = useMemo(
() => [...defaultSections, ...collectionSections],
[collectionSections, defaultSections]
);
const filteredSections = useMemo(() => {
let sections = allSections;
if (searchTerm.trim()) {
sections = sections.filter((section) =>
section.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}
const enabledSections = sections
.filter((section) => section.enabled)
.sort((a, b) => {
if (a.order !== undefined && b.order !== undefined) {
return a.order - b.order;
}
if (a.order !== undefined) return -1;
if (b.order !== undefined) return 1;
return 0;
});
const disabledSections = sections.filter((section) => !section.enabled);
return [...enabledSections, ...disabledSections];
}, [allSections, searchTerm]);
const getSectionId = (section: DashboardSectionOption) =>
`${section.type}-${section.collectionId ?? "default"}`;
const handleCheckboxChange = (section: DashboardSectionOption) => {
const enabledSections = allSections.filter((s) => s.enabled);
const highestOrder =
enabledSections.length > 0
? Math.max(...enabledSections.map((s) => s.order ?? 0))
: -1;
const updatedSections = allSections.map((s) => {
if (s.type === section.type && s.collectionId === section.collectionId) {
return {
...s,
enabled: !s.enabled,
order: !s.enabled ? highestOrder + 1 : undefined,
};
}
return s;
});
updateDashboardLayout.mutateAsync(updatedSections, {
onSettled: (data, error) => {
if (error) {
toast.error(error.message);
}
},
});
};
const handleReorder = (sourceId: string, destId: string) => {
if (sourceId === destId) return;
// Get only enabled sections for reordering
const enabledSections = filteredSections.filter((s) => s.enabled);
const sourceIndex = enabledSections.findIndex(
(s) => getSectionId(s) === sourceId
);
const destIndex = enabledSections.findIndex(
(s) => getSectionId(s) === destId
);
if (sourceIndex < 0 || destIndex < 0) return;
// Reorder only the enabled sections
const reorderedEnabled = [...enabledSections];
const [moved] = reorderedEnabled.splice(sourceIndex, 1);
reorderedEnabled.splice(destIndex, 0, moved);
// Assign new order values based on the reordered enabled sections
const reorderedWithNewOrders = reorderedEnabled.map((section, idx) => ({
...section,
order: idx,
}));
// Get disabled sections and combine with reordered enabled sections
const disabledSections = filteredSections.filter((s) => !s.enabled);
const updated = [...reorderedWithNewOrders, ...disabledSections];
updateDashboardLayout.mutateAsync(updated, {
onSettled: (data, error) => {
if (error) {
toast.error(error.message);
}
},
});
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) {
return;
}
const sourceId = active.id as string;
const destId = over.id as string;
// Only allow reordering enabled sections
const sourceSection = filteredSections.find(
(s) => getSectionId(s) === sourceId
);
const destSection = filteredSections.find(
(s) => getSectionId(s) === destId
);
if (sourceSection?.enabled && destSection?.enabled) {
handleReorder(sourceId, destId);
}
};
// Only include enabled sections in the sortable context
const sortableItems = filteredSections
.filter((section) => section.enabled)
.map(getSectionId);
return (
<DropdownMenu modal>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8">
<i className="bi-sliders2-vertical text-neutral" />
{t("edit_layout")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="min-w-72 pt-1 px-0 pb-0 select-none"
align="end"
>
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 mx-2">
<p className="text-sm text-neutral mb-1">
{t("display_on_dashboard")}
</p>
<TextInput
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="py-0 bg-base-100"
placeholder={t("search")}
/>
</div>
<DndContext
modifiers={[restrictToParentElement]}
onDragEnd={handleDragEnd}
sensors={sensors}
>
<SortableContext
items={sortableItems}
strategy={verticalListSortingStrategy}
>
<ul className="max-h-60 overflow-y-auto px-2 pb-2">
{filteredSections.map((section) => {
const color =
section.type === "COLLECTION"
? collections.find((c) => c.id === section.collectionId)
?.color
: undefined;
return (
<DraggableListItem
key={getSectionId(section)}
section={{ ...section, color }}
onCheckboxChange={handleCheckboxChange}
/>
);
})}
{filteredSections.length === 0 && (
<li className="text-sm py-2 text-center text-neutral">
{t("no_results_found")}
</li>
)}
</ul>
</SortableContext>
</DndContext>
</div>
</DropdownMenuContent>
</DropdownMenu>
);
}
interface DraggableListItemProps {
section: DashboardSectionOption & { color?: string };
onCheckboxChange: (section: DashboardSectionOption) => void;
}
function DraggableListItem({
section,
onCheckboxChange,
}: DraggableListItemProps) {
const sectionId = `${section.type}-${section.collectionId ?? "default"}`;
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: sectionId,
disabled: !section.enabled,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<li
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={cn(
"select-none py-1 px-1 flex items-center justify-between",
section.enabled
? "cursor-grab active:cursor-grabbing"
: "cursor-default",
isDragging && "opacity-50"
)}
>
<div className="flex items-center gap-2">
<input
id={`section-${section.type}-${section.collectionId ?? "default"}`}
className="checkbox checkbox-primary"
type="checkbox"
checked={section.enabled}
onChange={() => onCheckboxChange(section)}
/>
<label
htmlFor={`section-${section.type}-${
section.collectionId ?? "default"
}`}
className={`text-sm pointer-events-none ${
section.enabled ? "opacity-100" : "opacity-50"
}`}
>
<i
className={`bi-${
section.type === "STATS"
? "bar-chart-line"
: section.type === "RECENT_LINKS"
? "clock"
: section.type === "PINNED_LINKS"
? "pin"
: "folder-fill"
} ${section.type !== "COLLECTION" ? "text-primary" : ""} mr-1`}
style={
section.type === "COLLECTION" ? { color: section.color } : {}
}
/>
{section.name}
</label>
</div>
<i
className={`bi-grip-vertical text-neutral ${
section.enabled ? "opacity-100" : "opacity-50"
}`}
/>
</li>
);
}

View File

@@ -0,0 +1,241 @@
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
import useLocalSettingsStore from "@/store/localSettings";
import {
ArchivedFormat,
CollectionIncludingMembersAndLinkCount,
} from "@linkwarden/types";
import { useEffect, useRef, useState } from "react";
import unescapeString from "@/lib/client/unescapeString";
import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection";
import Image from "next/image";
import {
atLeastOneFormatAvailable,
formatAvailable,
} from "@linkwarden/lib/formatStats";
import useOnScreen from "@/hooks/useOnScreen";
import { useCollections } from "@linkwarden/router/collections";
import { useUser } from "@linkwarden/router/user";
import { useGetLink, useLinks } from "@linkwarden/router/links";
import { useRouter } from "next/router";
import openLink from "@/lib/client/openLink";
import LinkIcon from "./LinkViews/LinkComponents/LinkIcon";
import LinkFormats from "./LinkViews/LinkComponents/LinkFormats";
import LinkTypeBadge from "./LinkViews/LinkComponents/LinkTypeBadge";
import LinkPin from "./LinkViews/LinkComponents/LinkPin";
import { Separator } from "./ui/separator";
import { useDraggable } from "@dnd-kit/core";
import { cn } from "@linkwarden/lib";
import { useTranslation } from "next-i18next";
export function DashboardLinks({
links,
isLoading,
type,
}: {
links?: LinkIncludingShortenedCollectionAndTags[];
isLoading?: boolean;
type?: "collection" | "recent";
}) {
return (
<div
className={`flex gap-5 overflow-x-auto overflow-y-hidden hide-scrollbar w-full min-h-fit`}
>
{isLoading ? (
<div className="flex flex-col gap-4 min-w-60 w-60">
<div className="skeleton h-40 w-full"></div>
<div className="skeleton h-3 w-2/3"></div>
<div className="skeleton h-3 w-full"></div>
<div className="skeleton h-3 w-full"></div>
<div className="skeleton h-3 w-1/3"></div>
</div>
) : (
links?.map((e, i) => <Card key={i} link={e} dashboardType={type} />)
)}
</div>
);
}
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
editMode?: boolean;
dashboardType?: "collection" | "recent";
};
export function Card({ link, editMode, dashboardType }: Props) {
const { t } = useTranslation();
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: `${link.id}-${dashboardType}`,
data: {
linkId: link.id,
link,
dashboardType,
},
});
const { data: collections = [] } = useCollections();
const { data: user } = useUser();
const {
settings: { show },
} = useLocalSettingsStore();
const { links } = useLinks();
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
const { refetch } = useGetLink({ id: link.id as number, isPublicRoute });
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(
collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
useEffect(() => {
setCollection(
collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
}, [collections, links]);
const ref = useRef<HTMLDivElement>(null);
const isVisible = useOnScreen(ref);
const [linkModal, setLinkModal] = useState(false);
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (
isVisible &&
!link.preview?.startsWith("archives") &&
link.preview !== "unavailable"
) {
interval = setInterval(async () => {
refetch().catch((error) => {
console.error("Error refetching link:", error);
});
}, 5000);
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isVisible, link.preview]);
return (
<div
ref={setNodeRef}
className={cn(
isDragging ? "opacity-30" : "opacity-100",
"relative group touch-manipulation select-none"
)}
>
<div
ref={ref}
className={`min-w-60 w-60 border border-solid border-neutral-content bg-base-200 duration-100 rounded-xl relative group h-full`}
>
<div
className="rounded-xl cursor-pointer h-full w-full flex flex-col justify-between"
onClick={() =>
!editMode && openLink(link, user, () => setLinkModal(true))
}
{...listeners}
{...attributes}
>
{show.image && (
<div>
<div className={`relative rounded-t-xl h-40 overflow-hidden`}>
{formatAvailable(link, "preview") ? (
<Image
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true&updatedAt=${link.updatedAt}`}
width={1280}
height={720}
alt=""
className={`rounded-t-xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105`}
style={show.icon ? { filter: "blur(1px)" } : undefined}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
unoptimized
/>
) : link.preview === "unavailable" ? (
<div className={`bg-gray-50 h-40 bg-opacity-80`}></div>
) : (
<div
className={`h-40 bg-opacity-80 skeleton rounded-none`}
></div>
)}
{show.icon && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-xl flex items-center justify-center rounded-md">
<LinkIcon link={link} />
</div>
)}
{show.preserved_formats &&
link.type === "url" &&
atLeastOneFormatAvailable(link) && (
<div className="absolute bottom-0 right-0 m-2 bg-base-200 bg-opacity-60 px-1 rounded-md">
<LinkFormats link={link} />
</div>
)}
</div>
<Separator />
</div>
)}
<div className="flex flex-col justify-between h-full min-h-11">
<div className="p-3 flex flex-col justify-between h-full gap-2">
{show.name && (
<p className="line-clamp-2 w-full text-primary text-sm">
{unescapeString(link.name)}
</p>
)}
{show.link && <LinkTypeBadge link={link} />}
</div>
{(show.collection || show.date) && (
<div>
<Separator className="mb-1" />
<div className="flex justify-between items-center text-xs text-neutral px-3 pb-1 gap-2">
{show.collection && !isPublicRoute && (
<div className="cursor-pointer truncate">
<LinkCollection
link={link}
collection={collection}
isPublicRoute={false}
/>
</div>
)}
{show.date && <LinkDate link={link} />}
</div>
</div>
)}
</div>
</div>
{/* Overlay on hover */}
<div className="absolute pointer-events-none top-0 left-0 right-0 bottom-0 bg-base-100 bg-opacity-0 group-hover:bg-opacity-20 group-focus-within:opacity-20 rounded-xl duration-100"></div>
<LinkActions
link={link}
t={t}
linkModal={linkModal}
setLinkModal={(e) => setLinkModal(e)}
className="absolute top-3 right-3 group-hover:opacity-100 group-focus-within:opacity-100 opacity-0 duration-100 text-neutral z-20"
/>
{!isPublicRoute && <LinkPin link={link} />}
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More