Compare commits

...

94 Commits

Author SHA1 Message Date
ricoberger
7b84bab217 Prepare v1.3.0 Release 2024-02-12 19:51:12 +01:00
Rico Berger
1a56a8996e [core] Increase Update Interval for Reddit and Nitter (#138)
Reddit and Nitter are heavlily rate limited from time to time, to not
effect our premium users, by these limits, we decided to increase the
interval in which the Reddit and Nitter sources are updated for users
which are on the free tier.
2024-02-12 19:44:14 +01:00
Rico Berger
37bcc5e026 [nitter] Add Support for Piped Videos (#136)
The Nitter source now supports playing Piped Videos directly within the
app. For this we are checking if the Nitter post contains a piped.video
url and if this is the case we are using the newly added
`ItemPipedVideo` widget to render the video player, to allow users to
directly play the video within the app.

To support Piped videos we had to create a new `ItemPipedVideo` widget,
which is very similar to the `ItemYoutubeVideo` widget. This means on
the web version of FeedDeck we show the Video via an iframe and on the
other platforms via our `ItemVideoPlayer` widget. The main difference
between Piped and YouTube widget is the different client we use to fetch
the video urls. Besides the Piped API returns two different stream one
for the video and one for the audio, so that we had to add an additional
`audio` paramter to the `ItemVideoPlayer` widget, which allows us to
specify an additional audio source for a video.

NOTE: We had to add support for Piped, because Nitter automatically
converts the YouTube urls to the corresponding Piped urls.
2024-02-11 21:52:15 +01:00
Rico Berger
6a158f5176 [landing] Add Feature Images for Mobile Screens (#137)
Until now we always displayed the same feature images on the landing
page and it didn't matter if the user was on a desktop or mobile device.
This is now changed so that we display different images on mobile
devices (depending on the screen resolution).

Besides that we also moved the deprecated `themeColor` from the
`Metadata` to the `Viewport` and added it to all pages.

Last but not least we added some missing Android icons, which were
referenced in the `manifest.json` file, but where the icon didn't exist.
2024-02-11 21:26:47 +01:00
Rico Berger
0b7ca6cb14 [mastodon] Add Support for YouTube Videos (#135)
The Mastodon source supports YouTube Videos now. This means that if a
Mastodon post contains a link to a YouTube video as user can now
directly watch the video within the app.

If the post contains a video we render the YouTube video and the
description instead of the description, images and videos.
2024-02-10 21:25:16 +01:00
Rico Berger
303f78c3bc [reddit] Add Support for YouTube Videos (#134)
The Reddit source supports YouTube videos now. This means if a Reddit
post contains a link to a YouTube video, we render the video in the item
details, so that a user can directly watch the video.
2024-02-10 21:04:09 +01:00
ricoberger
c0c87e2c10 [core] Sort Imports 2024-02-10 20:43:18 +01:00
Rico Berger
babce57c80 [core] Fix Password Validations (#133)
This commit fixes the password validations. In #130 we introduced some
stronger password policies to forbid weak passwords, but forgot to
change it in all places. Now the same rules are also applying when a
user changes his password or resets his password.

During the sign in we do not use the same rules, to not block users
which have already signed up, with a password which doesn't match the
rules.
2024-02-10 15:31:56 +01:00
Rico Berger
ca5866ac13 [core] Add Continuous Delivery Workflow for Linux arm64 (#132)
Until now we had to manually build the app for Linux arm64 when we
created a new release. Now we are using ubicloud and the
`ubicloud-standard-2-arm` GitHub Action runner to build the app for
Linux arm64.
2024-02-10 14:58:27 +01:00
ricoberger
508e255c8b [core] Fix File 2024-02-06 21:05:40 +01:00
Rico Berger
aeeea4fd95 [core] Update macOS GitHub Action Runners (#131)
Update the used GitHub Action runners to `macos-14`, see
https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/
2024-02-02 17:36:46 +01:00
Rico Berger
6029ee539e [core] Forbid Weak Passwords (#130)
This commit improves our password policy, so that every user which signs
up must have a password with a minimum length of 8 characters, one upper
and lower case letter and one number.
2024-02-02 17:22:53 +01:00
Rico Berger
c9a596111c [rss] Do Not Remove HTML Tags (#129)
We do not remove HTML tags from the description of a RSS feed item
anymore, because:

- This didn't worked for all feeds, because we first removed the tags
  and then unescape the data, which didn't make sense
- We render the HTML to Makrdown in the frontend so that the description
  can contain HTML tags and is still properly rendered and in some case
  better rendered then before
2024-02-02 17:04:43 +01:00
Rico Berger
b8a73cc003 [core] Make Log Level Configurable (#128)
The log level is now configurable, via the `FEEDDECK_LOG_LEVEL`
environment variable. The environment variable can have the following
values: `debug`, `info`, `warning` or `error`.

With this change we also only log the response when getting and parsing
the feed fails when the log level is set to `debug`, also when the
actual message is still an error. This should reduce the noice in the
logs a lot and allows us to specially turn this on while debugging.
2024-02-02 16:52:14 +01:00
ricoberger
817eb4d9e8 [core] Update File 2024-02-02 16:06:44 +01:00
dependabot[bot]
20e3e736c2 Bump the docker group in /supabase/functions/_cmd with 1 update (#127)
Bumps the docker group in /supabase/functions/_cmd with 1 update: lukechannings/deno.


Updates `lukechannings/deno` from v1.40.2 to v1.40.3

---
updated-dependencies:
- dependency-name: lukechannings/deno
  dependency-type: direct:production
  dependency-group: docker
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 00:06:02 +01:00
dependabot[bot]
20352c0301 Bump the npm-landing group in /landing with 2 updates (#126)
Bumps the npm-landing group in /landing with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `@types/node` from 20.11.13 to 20.11.15
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 18.2.48 to 18.2.51
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: "@types/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 00:05:29 +01:00
Rico Berger
e7b7000f46 [core] Add Open Link Action (#125)
This commit adds a new action to the menu shown when a user clicks
longer on the item in a column. The new "Open Link" action allows a user
to directly open the link of the item, so that the details modal must
not be opened anymore to open the link.
2024-01-31 21:09:34 +01:00
Rico Berger
fb3bec623a [core] Add Missing SafeArea Widget (#124)
Several widgets which where rendered within a modal bottom sheet didn't
used a `SafeArea` widget in the body of the `Scaffold` widget, so that
the action buttons on the bottom of the widget where not rendered in the
correct position.
2024-01-31 20:54:50 +01:00
dependabot[bot]
976c066004 Bump the pub group in /app with 2 updates (#121)
* Bump the pub group in /app with 2 updates

Bumps the pub group in /app with 2 updates: [purchases_flutter](https://github.com/RevenueCat/purchases-flutter) and [supabase_flutter](https://github.com/supabase/supabase-flutter/tree/main/packages).


Updates `purchases_flutter` from 6.18.0 to 6.19.0
- [Release notes](https://github.com/RevenueCat/purchases-flutter/releases)
- [Changelog](https://github.com/RevenueCat/purchases-flutter/blob/6.19.0/CHANGELOG.md)
- [Commits](https://github.com/RevenueCat/purchases-flutter/compare/6.18.0...6.19.0)

Updates `supabase_flutter` from 2.3.1 to 2.3.2
- [Changelog](https://github.com/supabase/supabase-flutter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/supabase/supabase-flutter/commits/HEAD/packages)

---
updated-dependencies:
- dependency-name: purchases_flutter
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
- dependency-name: supabase_flutter
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
...

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

* Update Flutter Version

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ricoberger <mail@ricoberger.de>
2024-01-31 20:28:49 +01:00
dependabot[bot]
304a9744a9 Bump the npm-landing group in /landing with 1 update (#120)
Bumps the npm-landing group in /landing with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 20.11.10 to 20.11.13
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-31 19:51:34 +01:00
Rico Berger
ee11cae8dc [landing] Add Lemmy Icon (#123)
Add an icon for Lemmy to the landing page. The new `lemmy` source type
was added in #94 and is now also added to the landing page for the next
release which should happen very soon.
2024-01-31 19:37:50 +01:00
Rico Berger
04314f116d [core] Fix Naming of Files (#122)
Fix the naming of some files: Instead of login we are always using the
term sign in and already renamed the `DesktopLoginManager` to
`DesktopSignInManager` in #106. During the renaming we forgot to rename
the file which is now done.

We also renamed the `sign_in_with_apple.dart` file to
`signin_with_apple.dart`, because the other files also do not contain an
underscore between sign and in.
2024-01-31 19:32:23 +01:00
ricoberger
b645244378 [core] Fix Assignees in Dependabot Configuration 2024-01-31 19:12:09 +01:00
dependabot[bot]
110ff56aa1 Bump the pub group in /app with 9 updates (#119)
* Bump the pub group in /app with 9 updates

Bumps the pub group in /app with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) | `3.3.0` | `3.3.1` |
| [flutter_native_splash](https://github.com/jonbhanson/flutter_native_splash) | `2.3.8` | `2.3.10` |
| [http](https://github.com/dart-lang/http/tree/master/pkgs) | `1.1.2` | `1.2.0` |
| [intl](https://github.com/dart-lang/i18n/tree/main/pkgs) | `0.18.1` | `0.19.0` |
| [purchases_flutter](https://github.com/RevenueCat/purchases-flutter) | `6.5.1` | `6.18.0` |
| [supabase_flutter](https://github.com/supabase/supabase-flutter/tree/main/packages) | `2.0.2` | `2.3.1` |
| [url_launcher](https://github.com/flutter/packages/tree/main/packages/url_launcher) | `6.2.2` | `6.2.4` |
| [window_manager](https://github.com/leanflutter/window_manager) | `0.3.7` | `0.3.8` |
| [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) | `2.0.4` | `2.1.0` |


Updates `cached_network_image` from 3.3.0 to 3.3.1
- [Commits](https://github.com/Baseflow/flutter_cached_network_image/compare/v3.3.0...v3.3.1)

Updates `flutter_native_splash` from 2.3.8 to 2.3.10
- [Release notes](https://github.com/jonbhanson/flutter_native_splash/releases)
- [Changelog](https://github.com/jonbhanson/flutter_native_splash/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jonbhanson/flutter_native_splash/commits)

Updates `http` from 1.1.2 to 1.2.0
- [Release notes](https://github.com/dart-lang/http/releases)
- [Commits](https://github.com/dart-lang/http/commits/HEAD/pkgs)

Updates `intl` from 0.18.1 to 0.19.0
- [Release notes](https://github.com/dart-lang/i18n/releases)
- [Commits](https://github.com/dart-lang/i18n/commits/intl-v0.19.0/pkgs)

Updates `purchases_flutter` from 6.5.1 to 6.18.0
- [Release notes](https://github.com/RevenueCat/purchases-flutter/releases)
- [Changelog](https://github.com/RevenueCat/purchases-flutter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/RevenueCat/purchases-flutter/compare/6.5.1...6.18.0)

Updates `supabase_flutter` from 2.0.2 to 2.3.1
- [Changelog](https://github.com/supabase/supabase-flutter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/supabase/supabase-flutter/commits/HEAD/packages)

Updates `url_launcher` from 6.2.2 to 6.2.4
- [Release notes](https://github.com/flutter/packages/releases)
- [Commits](https://github.com/flutter/packages/commits/url_launcher-v6.2.4/packages/url_launcher)

Updates `window_manager` from 0.3.7 to 0.3.8
- [Release notes](https://github.com/leanflutter/window_manager/releases)
- [Changelog](https://github.com/leanflutter/window_manager/blob/main/CHANGELOG.md)
- [Commits](https://github.com/leanflutter/window_manager/commits)

Updates `youtube_explode_dart` from 2.0.4 to 2.1.0
- [Release notes](https://github.com/Hexer10/youtube_explode_dart/releases)
- [Changelog](https://github.com/Hexer10/youtube_explode_dart/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Hexer10/youtube_explode_dart/compare/v2.0.4...v2.1.0)

---
updated-dependencies:
- dependency-name: cached_network_image
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
- dependency-name: flutter_native_splash
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
- dependency-name: http
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
- dependency-name: intl
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
- dependency-name: purchases_flutter
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
- dependency-name: supabase_flutter
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
- dependency-name: url_launcher
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
- dependency-name: window_manager
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
- dependency-name: youtube_explode_dart
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
...

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

* Fix Podfile.lock

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ricoberger <mail@ricoberger.de>
2024-01-31 19:10:56 +01:00
ricoberger
bbbeb9524f [core] Remove Deprecate Edge Functions
This commit removes the deprecated edge functions `add-source-v1` and
`profile-v1` from the stage deployment.
2024-01-31 19:07:01 +01:00
Rico Berger
90fc7532ba [core] Add Client Side Scraping of Sources (#118)
It is now possible to add and update sources via client side scraping.
For that a new edge function `add-or-update-source-v1` was added and the
old `add-source-v1` function was deprecated.

The new function accepts a new `feedData` field, which can contain the
feed for a source. If the field is provided we will not try to get the
feed for a source within our edge function and instead use the provided
data.

Currently this function is only used to add a Reddit source. Later we
plan to extend it for other sources and want to use it to update source
via the app, when the source provider makes heavy use of rate limiting.
2024-01-30 21:59:42 +01:00
dependabot[bot]
fac622ef97 Bump the docker group in /supabase/functions/_cmd with 1 update (#114)
Bumps the docker group in /supabase/functions/_cmd with 1 update: lukechannings/deno.


Updates `lukechannings/deno` from v1.38.4 to v1.40.2

---
updated-dependencies:
- dependency-name: lukechannings/deno
  dependency-type: direct:production
  dependency-group: docker
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 21:49:44 +01:00
dependabot[bot]
e29b94a576 Bump the github-actions group with 4 updates (#110)
Bumps the github-actions group with 4 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/configure-pages](https://github.com/actions/configure-pages), [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) and [actions/deploy-pages](https://github.com/actions/deploy-pages).


Updates `actions/upload-artifact` from 3 to 4
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

Updates `actions/configure-pages` from 3 to 4
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v3...v4)

Updates `actions/upload-pages-artifact` from 2 to 3
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v2...v3)

Updates `actions/deploy-pages` from 2 to 4
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/configure-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-pages-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/deploy-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 21:49:03 +01:00
dependabot[bot]
911b3691b3 Bump the npm-landing group in /landing with 11 updates (#116)
Bumps the npm-landing group in /landing with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@headlessui/react](https://github.com/tailwindlabs/headlessui/tree/HEAD/packages/@headlessui-react) | `1.7.17` | `1.7.18` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.10.1` | `20.11.10` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.39` | `18.2.48` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `18.2.17` | `18.2.18` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.4.16` | `10.4.17` |
| [eslint](https://github.com/eslint/eslint) | `8.54.0` | `8.56.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `14.0.3` | `14.1.0` |
| [next](https://github.com/vercel/next.js) | `14.0.3` | `14.1.0` |
| [postcss](https://github.com/postcss/postcss) | `8.4.31` | `8.4.33` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss) | `3.3.5` | `3.4.1` |
| [typescript](https://github.com/Microsoft/TypeScript) | `5.3.2` | `5.3.3` |


Updates `@headlessui/react` from 1.7.17 to 1.7.18
- [Release notes](https://github.com/tailwindlabs/headlessui/releases)
- [Changelog](https://github.com/tailwindlabs/headlessui/blob/main/packages/@headlessui-react/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/headlessui/commits/@headlessui/react@v1.7.18/packages/@headlessui-react)

Updates `@types/node` from 20.10.1 to 20.11.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 18.2.39 to 18.2.48
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 18.2.17 to 18.2.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

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

Updates `eslint` from 8.54.0 to 8.56.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.54.0...v8.56.0)

Updates `eslint-config-next` from 14.0.3 to 14.1.0
- [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/commits/v14.1.0/packages/eslint-config-next)

Updates `next` from 14.0.3 to 14.1.0
- [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/v14.0.3...v14.1.0)

Updates `postcss` from 8.4.31 to 8.4.33
- [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.31...8.4.33)

Updates `tailwindcss` from 3.3.5 to 3.4.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.5...v3.4.1)

Updates `typescript` from 5.3.2 to 5.3.3
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.3.2...v5.3.3)

---
updated-dependencies:
- dependency-name: "@headlessui/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: "@types/node"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
- dependency-name: "@types/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: "@types/react-dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: autoprefixer
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: eslint
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
- dependency-name: eslint-config-next
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
- dependency-name: postcss
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: tailwindcss
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
- dependency-name: typescript
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 21:48:09 +01:00
Rico Berger
3a84376223 Add Assignees to Dependabot Configuration (#113) 2024-01-30 19:41:03 +01:00
Rico Berger
0f5a8e44f1 [core] Add Additional Header for Web Deployment (#112)
This commit adds the `X-Content-Type-Options`, `X-Frame-Options` and
`Referrer-Policy` headers to the web deployment of FeedDeck via the
`_headers` file.

See https://developers.cloudflare.com/pages/configuration/headers/
2024-01-30 19:27:47 +01:00
Rico Berger
5753fb2714 Update Flutter to Version 3.16.5 (#106)
Update the used Flutter version to 3.16.5 and the used packages to their
latest version.

The Supabase package contained some breaking changes:
- `functionUrl` is not exported anymore, so that it must be generated by
  ourselfs
- `Provider` was renamed to `OAuthProvider`
- The `signInWithApple` method was removed and is now implemented by us
  via the `sign_in_with_apple` package.

We also renamed the `DesktopLoginManager` to `DesktopSignInManager` to
use the same naming as in other places of the app, where we are always
using sign in and not login.
2023-12-21 17:24:07 +01:00
ricoberger
4008660a35 [core] Fix deno test Commands in Contributing Guidelines 2023-12-20 09:47:27 +01:00
Rico Berger
1cb58e1e0f [core] Refactor Tools and add get-feed Tool (#105)
This commit refactors the existing tools, by moving the tools logic to a
new `tools.ts` file, so that the main `cmd.ts` file remains clear.

Besides that we also add a new tool `get-feed` which can be used to run
the `getFeed` function from the command line. The function is called
with a source and returns the generated source and items, as they are
saved in the database by the `add-source-v1` Supabase edge function.
2023-12-17 18:04:53 +01:00
ricoberger
08e9170a80 [nitter] Adjust Header Handling 2023-12-17 13:51:03 +01:00
Rico Berger
4198a5bac6 [core] Replace Deprecated serve Function (#104)
The used `serve` function from the `std/server` module is deprecated and
must be replaced with `Deno.serve`. This was done within this commit.
2023-12-17 12:37:00 +01:00
ricoberger
295ae13705 [core] Remove deno.lock File 2023-12-17 12:11:31 +01:00
Rico Berger
0894f0e777 [core] Improve Error Handling for Feed Edge Functions (#103)
This commit improve the error handling for the edge function to add a
new source and the worker, so that we get more insights why a request
fails. A user will now also get a more detailed error why a source could
not be added.
2023-12-17 12:10:04 +01:00
ricoberger
2966ecc651 [rss] Fix Test Name 2023-12-12 22:13:47 +01:00
Rico Berger
49c168b5b2 [core] Disable Right Click for Item Actions on Web (#102)
On the web the right click on an item to show the actions, doesn't work
properly, because the browsers right click menu will be shown first. So
it doesn't make sense to show also our right click menu.
2023-12-12 22:03:32 +01:00
Rico Berger
8e0017e928 [rss] Show Videos from RSS Feeds (#101)
If an RSS feed contains a video within the `attachments` field, the video
will now be added to the `options` field of the item. In the Flutter
code we then check if the video field is present in the options and show
the video instead of an image in the details view of the item.
2023-12-12 21:49:45 +01:00
Rico Berger
982add8fbb [core] Update Deno Modules (#100)
This commit updates all used Deno modules to their latest version.

Since some of the used modules / functions were deprecated we had to
adjust our encrypt / descrypt functions and the generation of the source
and item ids, where we have to use a new md5 function.
2023-12-12 20:32:44 +01:00
Rico Berger
8065e19c85 [rss] Parse Atom and RDF Feeds from Websites (#99)
Until now we only checked if a website contained a RSS feed which can be
used for FeedDeck. Now we are also checking if the website contains a
Atom or RDF feed when no RSS feed was found.

For this we are checking `link` tag with the `type="application/atom+xml"`
attribute or a link tag with the `type="application/rdf+xml"` attribute.
2023-12-12 20:22:30 +01:00
Rico Berger
9e59439226 [core] Add Tests for Sources (#98)
This commit adds tests for all available sources.

This commit also fixes the parsing of Atom feeds for the RSS source,
where the `dc:date` field must be used for the `publishedAt` field.
2023-12-12 18:50:29 +01:00
Rico Berger
5087c299d3 [core] Add Test Setup for Deno (#97)
This commit uses the "Continuous Integration" GitHub Action, to run
tests for the Deno code, which is used by the Supabase functions and our
Docker containers.

This commit also adds a first test so that the `deno test` command does
not fail.
2023-12-03 15:55:04 +01:00
Rico Berger
bddf5874d4 [core] Fix Converting of HTML to Plain Text in Description (#96)
This commit fixes the conversion of HTML to plain text in the
description for an item. Until now it could happen, that the there was
no whitespace between some words after the conversion. This is now fixed
so that there is always a whitespace between words in the plain text.
2023-12-02 17:58:44 +01:00
Rico Berger
6c469e5d0d [core] Improve ItemVideoPlayer Widget (#95)
This commit adds two improvements to the `ItemVideoPlayer` widget. These
improvements are:
1. The padding for the widget is now defined within the widget, so that
   is must not be defined in the parent widget. With this change the
   widget follows the styling of our other widgets like `ItemMedia`.
2. On iOS the quality selection had a large bottom padding, this is now
   fixed, by using a `Wrap` widget instead of a `ListView` like we are
   using in the other modal bottom sheets which are showing some
   actions.
2023-12-02 16:51:56 +01:00
Rico Berger
8c88ece3dc [lemmy] Add Support for Lemmy (#94)
This commit adds support to add Lemmy RSS feeds to FeedDeck. A user can
provide the url of an Lemmy instance, the url of a community or of an
user.

The special thing of the Lemmy source in opposite to the normal RSS
source is, that we parse the provided link form a feed item, to check if
it contains a image, video or YouTube url, to apply some special
formatting.
2023-12-02 15:52:50 +01:00
Rico Berger
eb28a44cc8 [core] Fix Index Reset for Tabs in Small Deck Layout (#93)
The index was not reset in the `DeckLayoutSmall` widget, when the user
selected a new deck in the settings widget. This was caused because the
`DefaultTabController` was not rebuild after a new deck was selected, so
that the `initialIndex` value was not used.

This is now fixed by adding a `key` to the `DefaultTabController`, which
corresponds to the selected deck. This means if the user selects a new
deck in the settings the widget will be rebuild and the initial selected
tab will be the first one. If a user selects the same deck or switches
between the small and large layout the tab will be the formerly selected
one.
2023-12-01 18:09:54 +01:00
ricoberger
240e9e93d9 [podcast] Add Comment for the _player.pause() Hack 2023-12-01 18:03:28 +01:00
dependabot[bot]
eebec73fd2 Bump the npm-email-templates group in /supabase/email-templates with 1 update (#89)
* Bump the npm-email-templates group

Bumps the npm-email-templates group in /supabase/email-templates with 1 update: [@react-email/components](https://github.com/resendlabs/react-email/tree/HEAD/packages/components).

- [Release notes](https://github.com/resendlabs/react-email/releases)
- [Changelog](https://github.com/resendlabs/react-email/blob/canary/docs/changelog.mdx)
- [Commits](https://github.com/resendlabs/react-email/commits/v0.0.11/packages/components)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-email-templates
...

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

* Run `npm install`

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ricoberger <mail@ricoberger.de>
2023-12-01 17:38:16 +01:00
dependabot[bot]
1b226791b4 Bump the pub group in /app with 1 update (#90)
Bumps the pub group in /app with 1 update: [msix](https://github.com/YehudaKremer/msix).

- [Release notes](https://github.com/YehudaKremer/msix/releases)
- [Changelog](https://github.com/YehudaKremer/msix/blob/main/CHANGELOG.md)
- [Commits](https://github.com/YehudaKremer/msix/compare/v3.16.6...v3.16.7)

---
updated-dependencies:
- dependency-name: msix
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 16:48:48 +01:00
dependabot[bot]
ad9885ce92 Bump the npm-landing group in /landing with 7 updates (#91)
Bumps the npm-landing group in /landing with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.8.10` | `20.10.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.33` | `18.2.39` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `18.2.14` | `18.2.17` |
| [eslint](https://github.com/eslint/eslint) | `8.52.0` | `8.54.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `14.0.1` | `14.0.3` |
| [next](https://github.com/vercel/next.js) | `14.0.1` | `14.0.3` |
| [typescript](https://github.com/Microsoft/TypeScript) | `5.2.2` | `5.3.2` |


Updates `@types/node` from 20.8.10 to 20.10.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 18.2.33 to 18.2.39
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 18.2.14 to 18.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `eslint` from 8.52.0 to 8.54.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.52.0...v8.54.0)

Updates `eslint-config-next` from 14.0.1 to 14.0.3
- [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/commits/v14.0.3/packages/eslint-config-next)

Updates `next` from 14.0.1 to 14.0.3
- [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/v14.0.1...v14.0.3)

Updates `typescript` from 5.2.2 to 5.3.2
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.2.2...v5.3.2)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
- dependency-name: "@types/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: "@types/react-dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: eslint
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
- dependency-name: eslint-config-next
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: typescript
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 16:44:20 +01:00
dependabot[bot]
16418ab205 Bump the docker group in /supabase/functions/_cmd with 1 update (#92)
Bumps the docker group in /supabase/functions/_cmd with 1 update: lukechannings/deno.


---
updated-dependencies:
- dependency-name: lukechannings/deno
  dependency-type: direct:production
  dependency-group: docker
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 16:38:50 +01:00
Rico Berger
d9d82a1679 [core] Add Test Setup for Flutter (#87)
This commit adds a new "Continuous Integration" GitHub Action, which is
used to run the tests for the Flutter app (and later also for Deno).
This commit also adds a first test, so that the GitHub Action does not
fail.

This PR also removes the Visual Studio Code and Neovim configurations
from the contribution guide, since I'm not using it anymore and for that
I do not want to maintain it any longer.
2023-11-30 21:49:49 +01:00
ricoberger
ff52516324 Prepare v1.2.1 Release 2023-11-30 18:03:33 +01:00
Rico Berger
abd3c24f68 [core] Remove Blank in Item Preview Description (#86)
This commit removes all blank lines in the item preview description, so
that we do not render a blank line as the last line. This was done to
improve the style of the item previews, which looked ugly when the last
line was a blank line.
2023-11-29 21:47:53 +01:00
Rico Berger
04ef618295 [core] Add Right Click Support for Item Actions (#85)
Until now it was only possible to show the actions for an item by
pressing longer on the item. Now a user can also right click on the item
to display the actions which feels a bit more natural on desktop
devices.
2023-11-29 21:14:27 +01:00
Rico Berger
a58c93be8a [podcast] Stop Audio Playback on Windows and Linux (#84)
On Windows and Linux it could happen that the audio playback for a
podcast wasn't stopped when the item details view for a podcast item was
closed.

This commit "fixes" the problem, by pausing the audio player, before the
widget is disposed.

This commit also fixes the condition when the background audio services
should be initialized in the `main.dart` file. Instead of the macOS
check, we checked for iOS twice.
2023-11-29 21:06:06 +01:00
Rico Berger
5a8d6b34c1 [medium] Extend Filter Words List (#83) 2023-11-29 20:09:34 +01:00
ricoberger
9233e4d373 [core] Fix 2023-11-26 20:07:18 +01:00
Rico Berger
6e50af16a7 [core] Fix build.gradle File for Android Release (#82)
The change introduced in #71 so that we can run the Android build in a
GitHub action, broke the `flutter build appbundle` command to build the
Android version for the Google Play store. This commit should fix this,
so that we can build the Android version in a GitHub Action and for
Google Play.
2023-11-26 19:40:54 +01:00
ricoberger
046479071b Prepare v1.2.0 Release 2023-11-26 17:51:09 +01:00
Rico Berger
ffbed73669 [medium] Remove Spam (#79)
When using Medium as a source it is possible that the a lot of items
might be spam when following a tag. To reduce the spam, we filter these
items based on a word list and an calculated score. The score is
calculated by the number of words which are included in the title from
our word list.
2023-11-26 17:03:54 +01:00
Rico Berger
03371cf645 [reddit] Remove Tables from Description (#78)
When a Reddit post contained an image it was always rendered as table,
where the image was displayed in the left column and the author in the
right column. This looked very ugly in the app, so that we are now
removing all tables from the description of an Reddit post, so that the
image is rendered as large as possible and the author is displayed below
the image.
2023-11-26 16:55:17 +01:00
Rico Berger
eb606b5f6c [core] Improve Subtitle in Details View (#77)
This commit improves the displayed subtitle in the details view of an
item. For that we have adjusted the `ItemSubtitle` widget to diplay next
to the source title, author and publishing time an corresponding icon.
We also increased the space between the items and we are using a `|`
instead of a `/` as seperator.
2023-11-26 16:09:48 +01:00
Rico Berger
df927516b1 [core] Improve Icon Handling (#76)
Instead of defining the icons for a source only within the `SourceIcon`
widget, the icons are now defined as extension for the `FDSourceType`
enum. The background and foreground colors are also defined within the
enum now. This allows us to access the icon of a source outside of the
`SourceIcon` widget and we only have to touch the `source.dart` file
when adding a new source type.
2023-11-26 14:52:15 +01:00
Rico Berger
fa23e095e5 [core] Fix getMedia Function (#75)
In the `getMedia` function for Medium, Nitter, RSS and Tumblr we checked
the content and/or description of an RSS feed entry for a media file,
but we didn't pass the string to the `unescape` function first, so that
we might missed some media files, because our regular expression were
not able to find an image.

Now we are using the `unescape` function before using our regular
expression to find the image, similar to how we are also applying the
`unescape` function before we save the item description.
2023-11-25 18:24:22 +01:00
Rico Berger
2298176c3b [pinterest] Add Support for Pinterest (#74)
This commit adds a new source type "pinterest", which can be used to
follow the post of an user or a board on Pinterest. To use the new
source type a user can select the "Pinterest" item in the add source
modal. In the form a user can provide the username or board he wants to
follow via FeedDeck.

In the corresponding Supabase function we then convert the input
provided by the user to an valid RSS feed url for Pinterest. This means
that we have to add `/feed.rss` for users and `.rss` for boards to the
Pinterest url.

Then we generate the source and items as for the other sources and reuse
the existing components to render the preview and details item. We had
to adjust the rendering logic for these items, to ignore empty values,
from which also other sources will benefit.
2023-11-25 18:04:19 +01:00
Rico Berger
3f7caf4ad4 [core] Fix Decoding of Special Characters (#73)
We have to add the "charset" parameter to the "Content-Type" header when
we return json from one of the Supabase functions, so that special
characters are properly decoded in Flutter.
2023-11-25 00:00:22 +01:00
Rico Berger
db9363e7af [rss] Allow Users to Provide a Website URL (#72)
Users can now provide the URL of a website instead of the url of a RSS
feed via the input field for the RSS source.

This is possible because we are now trying to get and parse the RSS feed
for the provided url as usual, but if this operation fails, we try to
parse the text as html, so that we can check if it contains a
"<link type="application/rss+xml" href="RSS_FEED_URL">" tag. If this is
the case we are using this value to try to get and parse the RSS feed
again. If it fails again we are returning an error as before.
2023-11-24 23:11:50 +01:00
Rico Berger
ce36761c64 [core] Add GitHub Action for iOS and Android (#71)
This commit adds 2 new jobs to the existing "Continuous Delivery" GitHub
Action to test the build of the iOS and Android app.

For this we also had to adjust the "build.gradle" file for the Android
app, so that the test build in the GitHub Action is signed with dummy
credentials, since we do not provide the keystore properties.
2023-11-24 17:37:24 +01:00
Rico Berger
4e38cfdb5c [core] Update Flutter to Version 3.16.0 (#70)
Update the used Flutter version to 3.16.0 and all Flutter packages to
their latest version.

This commit also fixes all of the newly added analysis options and the
layout changes introduced with the new Flutter version (e.g. we have to
set the "tabAlignment" property in the "TabBar" widget).
2023-11-24 17:12:08 +01:00
Rico Berger
1c7c88a9ca [core] Submit "Add Source" Forms on Enter (#69)
All our forms were submitable via enter except the forms used to add a
new source to a column. This is now changed so that also these forms can
be submitted by pressing enter in a text field.
2023-11-07 23:12:46 +01:00
dependabot[bot]
c7d208de23 Bump the pub group in /app with 8 updates (#68)
* Bump the pub group in /app with 8 updates

Bumps the pub group in /app with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [flutter_lints](https://github.com/flutter/packages/tree/main/packages) | `2.0.3` | `3.0.1` |
| [flutter_native_splash](https://github.com/jonbhanson/flutter_native_splash) | `2.3.3` | `2.3.5` |
| [msix](https://github.com/YehudaKremer/msix) | `3.16.4` | `3.16.6` |
| [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus) | `4.1.0` | `4.2.0` |
| [purchases_flutter](https://github.com/RevenueCat/purchases-flutter) | `6.0.0` | `6.2.0` |
| [supabase_flutter](https://github.com/supabase/supabase-flutter/tree/main/packages) | `1.10.22` | `1.10.24` |
| [timeago](https://github.com/andresaraujo/timeago.dart) | `3.5.0` | `3.6.0` |
| [url_launcher](https://github.com/flutter/packages/tree/main/packages/url_launcher) | `6.1.14` | `6.2.1` |


Updates `flutter_lints` from 2.0.3 to 3.0.1
- [Release notes](https://github.com/flutter/packages/releases)
- [Commits](https://github.com/flutter/packages/commits/pigeon-v3.0.1/packages)

Updates `flutter_native_splash` from 2.3.3 to 2.3.5
- [Release notes](https://github.com/jonbhanson/flutter_native_splash/releases)
- [Changelog](https://github.com/jonbhanson/flutter_native_splash/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jonbhanson/flutter_native_splash/commits/v2.3.5)

Updates `msix` from 3.16.4 to 3.16.6
- [Release notes](https://github.com/YehudaKremer/msix/releases)
- [Changelog](https://github.com/YehudaKremer/msix/blob/main/CHANGELOG.md)
- [Commits](https://github.com/YehudaKremer/msix/compare/v3.16.4...v3.16.6)

Updates `package_info_plus` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/fluttercommunity/plus_plugins/releases)
- [Commits](https://github.com/fluttercommunity/plus_plugins/commits/package_info_plus-v4.2.0/packages/package_info_plus)

Updates `purchases_flutter` from 6.0.0 to 6.2.0
- [Release notes](https://github.com/RevenueCat/purchases-flutter/releases)
- [Changelog](https://github.com/RevenueCat/purchases-flutter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/RevenueCat/purchases-flutter/compare/6.0.0...6.2.0)

Updates `supabase_flutter` from 1.10.22 to 1.10.24
- [Changelog](https://github.com/supabase/supabase-flutter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/supabase/supabase-flutter/commits/supabase_flutter-v1.10.24/packages)

Updates `timeago` from 3.5.0 to 3.6.0
- [Commits](https://github.com/andresaraujo/timeago.dart/commits)

Updates `url_launcher` from 6.1.14 to 6.2.1
- [Release notes](https://github.com/flutter/packages/releases)
- [Commits](https://github.com/flutter/packages/commits/url_launcher-v6.2.1/packages/url_launcher)

---
updated-dependencies:
- dependency-name: flutter_lints
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: pub
- dependency-name: flutter_native_splash
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
- dependency-name: msix
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
- dependency-name: package_info_plus
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
- dependency-name: purchases_flutter
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
- dependency-name: supabase_flutter
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pub
- dependency-name: timeago
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
- dependency-name: url_launcher
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pub
...

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

* Update Flutter Packages

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ricoberger <mail@ricoberger.de>
2023-11-05 23:33:01 +01:00
dependabot[bot]
a7639344a1 Bump the npm-email-templates group in /supabase/email-templates with 1 update (#61)
* Bump the npm-email-templates group

Bumps the npm-email-templates group in /supabase/email-templates with 1 update: [@react-email/components](https://github.com/resendlabs/react-email/tree/HEAD/packages/components).

- [Release notes](https://github.com/resendlabs/react-email/releases)
- [Changelog](https://github.com/resendlabs/react-email/blob/main/docs/changelog.mdx)
- [Commits](https://github.com/resendlabs/react-email/commits/HEAD/packages/components)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-email-templates
...

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

* Update Email Templates

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ricoberger <mail@ricoberger.de>
2023-11-05 23:07:07 +01:00
dependabot[bot]
ff76531962 Bump the npm-landing group in /landing with 7 updates (#60)
Bumps the npm-landing group in /landing with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.8.5` | `20.8.10` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.28` | `18.2.33` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `18.2.13` | `18.2.14` |
| [eslint](https://github.com/eslint/eslint) | `8.51.0` | `8.52.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `13.5.4` | `14.0.1` |
| [next](https://github.com/vercel/next.js) | `13.5.4` | `14.0.1` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss) | `3.3.3` | `3.3.5` |


Updates `@types/node` from 20.8.5 to 20.8.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 18.2.28 to 18.2.33
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 18.2.13 to 18.2.14
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `eslint` from 8.51.0 to 8.52.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.51.0...v8.52.0)

Updates `eslint-config-next` from 13.5.4 to 14.0.1
- [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/commits/v14.0.1/packages/eslint-config-next)

Updates `next` from 13.5.4 to 14.0.1
- [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.5.4...v14.0.1)

Updates `tailwindcss` from 3.3.3 to 3.3.5
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.3...v3.3.5)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: "@types/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: "@types/react-dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
- dependency-name: eslint
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-landing
- dependency-name: eslint-config-next
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-landing
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-landing
- dependency-name: tailwindcss
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-landing
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-05 23:04:13 +01:00
dependabot[bot]
dc77f16a34 Bump the github-actions group with 1 update (#62)
* Bump the github-actions group with 1 update

Bumps the github-actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node).

- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

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

* Update Node Version

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ricoberger <mail@ricoberger.de>
2023-11-05 22:59:13 +01:00
Rico Berger
bcd03e7e60 [core] Add Custom Cache Manager (#67)
This commit adds a custom cache manager "CustomCacheManager" which is
used in the "CachedNetworkImage" widget. The custom cache manager is
required, so that we can adjust the stale periode of cached images. By
default the package used 30 days as stale periode, but for our use case
7 days should be enough and we can reduce the storage used by the app.

Note: We also fixed the "run.sh" script to work with devices where the
name contains a space.
2023-11-05 17:35:37 +01:00
Rico Berger
92bea5d715 [core] Improve Media Handling (#66)
This commit improves / simplifies the media handling within the app.
Until now we always had to provide the type of the media file (item
media / source icon) when we wanted to display it. This is now not
necessary anymore. Instead we always display the image from it's
original path when the url starts with "http://" or "https://".
Additionally we also check the platform to proxy the image request on
the web. If the image doesn't start with "http://" or "https://" we
always try to display the image from the Supabase storage.

For this we also adjusted the corresponding Deno function, so that we
check if the image starts with "http://" or "https://" before we upload
it to the Supabase storage. If this is the case we upload the source
icon to the Supabase storage. If the upload fails, we will use the
original path of the icon.

Last but not least this commit also introduces our own
"CachedNetworkImage" widget, which wraps the original
"CachedNetworkImage" widget. Our own widget will ensure that we use the
correct url for the image request, so that we do not have to use this
function all over the app anymore. Later this widget can also be used to
introduce our own cache manager.
2023-11-04 18:27:17 +01:00
Rico Berger
37cd4dff6f [core] Run "deno fmt" (#65)
Format the Deno code via "deno fmt" to use the defined code style from
the "deno.json" file.
2023-11-04 15:45:42 +01:00
Rico Berger
d62bf10eaf [github] Fix Icons in Item Preview (#64)
The icons for GitHub items were not shown, because we did not set the
correct "sourceIconType", which was required for the "ItemSource"
widget.
2023-11-04 11:48:12 +01:00
Rico Berger
3afbe5674b [core] Add Development Setup for Neovim (#63)
Add some stuff to make the development with Neovim easier:

- Add a "deno.json" file, which is required by the Deno language server,
  when working in Neovim to detect the "import_map.json" file.
- Add a "run.sh" script to easily run the Flutter app from the command
  line.
- Add the "run.sh" script and an example for Neovim to the contributing
  guide.
2023-11-04 11:47:53 +01:00
Rico Berger
6d5a699db6 [core] Add Missing Divider to Video Quality Selection (#58)
In the modal bottom sheet where a user can select the video quality the
dividers between the different qualities were missing. This commit adds
the missing divider, so that the modal bottom sheet looks similar to the
other modal bottom sheets we are using (e.g. sign out, account
settings).
2023-10-30 19:49:21 +01:00
Rico Berger
e19885a594 [core] Fix Modal Bottom Sheet Size for Images (#57)
The size of the modal bottom sheet to display images had always a max
width of 640px on large screens. This wasn't intended an the modal
bottom sheet should fill the whole screen. This is now fixed, so that
when a user clicks on an image in the details view of an item, the whole
screen is used to display the image.
2023-10-30 12:08:57 +01:00
Rico Berger
6e27eb751c [youtube] Add Desktop Support (#56)
It is now pissible to play YouTube videos on the native desktop clients.
To achieve this we removed the "youtube_player_iframe" package which was
used before to play YouTube video, but which only supported web, iOS and
Android as target platforms.

On the web we are now using our own implementation to render an iframe
with for the YouTube video.

On all other platforms we are now using the "youtube_explode_dart"
package to fetch the video urls for a YouTube video and then we display
them within our own video player (the "ItemVideoPlayer" widget which was
added in #51). We also decided to switch the package for the iOS and
Android implementation which already worked before, because we are now
able to play YouTube videos in fullscreen and we only have to maintain
an exception for the web implementation.

The "ItemVideoPlayer" widget now also supports multiple qualities of an
video via the "qualities" paramter, which allows a user to switch
between the different video qualities which are available for a YouTube
video. Last but not least the widget now uses our primary color for the
seek bar.
2023-10-29 22:48:54 +01:00
Rico Berger
8e3586f315 [github] Fix Notification Links for PRs (#55)
If a notification was related to a pull request the link we added to the
item didn't work. This should now be fixed, so that when a user clicks
on the item he is redirected to the correct pull request which is
associated with the item.

This commit also removes an unnecessary "console.log" statement.
2023-10-29 13:49:06 +01:00
Rico Berger
e994ab6214 [core] Sign Out from Current Device (#54)
Until now a user was always signed out from all devices when he clicked
on the "Sign Out" button in the settings. Now we will display a actions
modal bottom sheet where a user can select if he wants to sign out from
the current device or from all devices.

For this we pass the [SignOutScope] to the "signOut" function of
Supabase. if the scope is "local" the user will be signed out from the
current device. If the scope is "global" the user will be signed out
from all devices.
2023-10-29 10:39:28 +01:00
Rico Berger
4906f9dc27 [core] Improve Tabs Handling in Small Deck Layout (#53)
This commit improves the handling of tabs in the samll deck layout. We
are now saving the selected tabs index in the newly added
"LayoutRepository" so that we can reuse the selected tab when a user
switches between the small and large layout. We can now also set the tab
which should be initially selected in the large layout when a user
selects a column in the navigation rail. Last but not least we can also
reset the initial tab index when a user selects a new deck in the
settings, so that we always display the first column instead of the
column with the same index as it was selected in the previous deck.
2023-10-28 17:36:01 +02:00
Rico Berger
55c6da07d9 [rss] Improve Rendering of Items (#52)
The items of an RSS feed are now rendered better, to achieve this we did
the following changes:

- Remove leading and trailing whitespaces from the item description
  which should be rendered.
- Check if the media file of an item is an SVG image. If this is the
  case we will not add it to the "media" field in the database, because
  currently the CachedNetworkImage widget can not render SVGs. If we
  want to render them, we run into serious performance issue so we skip
  them completly.
- Always assume that the content of an RSS feed contains HTML and render
  them as plain text in the preview and as markdown in the details.
  Since we also render images from the description now, we check if the
  "item.media" image should be rendered. If the description contains an
  image we do not render our own image. If the description doesn't
  contain a image we render it.
2023-10-28 11:37:00 +02:00
Rico Berger
8dc83a5d5a [mastodon] Add Support for Videos (#51)
It is now possible to play videos from toots within FeedDeck. For that
we are using the "madia_kit" package, which is already used for the
Podcast player on Windows and Linux.

The videos from a toot are saved within the "options.videos" field of an
item next to the "options.media" field. In the "ItemDetailsMastodon"
widget we are then checking if this field is present and contains a list
of video urls. These urls can then be played via the "ItemVideos"
widget.
2023-10-27 15:22:27 +02:00
ricoberger
94d5732f6a Fix App IDs used in Landing Page 2023-10-22 23:09:37 +02:00
ricoberger
f4a9f84061 Fix app.feeddeck.feeddeck.metainfo.xml 2023-10-22 20:28:10 +02:00
210 changed files with 14018 additions and 3882 deletions

View File

@@ -4,6 +4,8 @@ updates:
directory: "/"
schedule:
interval: "monthly"
assignees:
- "ricoberger"
groups:
github-actions:
patterns:
@@ -13,6 +15,8 @@ updates:
directory: "/app"
schedule:
interval: "monthly"
assignees:
- "ricoberger"
groups:
pub:
patterns:
@@ -22,6 +26,8 @@ updates:
directory: "/supabase/functions/_cmd"
schedule:
interval: "monthly"
assignees:
- "ricoberger"
groups:
docker:
patterns:
@@ -31,6 +37,8 @@ updates:
directory: "/landing"
schedule:
interval: "monthly"
assignees:
- "ricoberger"
groups:
npm-landing:
patterns:
@@ -40,6 +48,8 @@ updates:
directory: "/supabase/email-templates"
schedule:
interval: "monthly"
assignees:
- "ricoberger"
groups:
npm-email-templates:
patterns:

View File

@@ -87,11 +87,12 @@ jobs:
supabase db push
supabase functions deploy add-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy add-or-update-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
# supabase functions deploy add-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy delete-user-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy generate-magic-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy image-proxy-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy profile-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
# supabase functions deploy profile-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy profile-v2 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy revenuecat-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy stripe-create-billing-portal-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
@@ -109,6 +110,7 @@ jobs:
supabase db push
supabase functions deploy add-or-update-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy add-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy delete-user-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
supabase functions deploy generate-magic-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
@@ -166,7 +168,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.13.7'
flutter-version: '3.16.9'
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
@@ -194,7 +196,7 @@ jobs:
# runs for pull requests and when a new release is published.
macos:
name: macOS
runs-on: macos-latest
runs-on: macos-14
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
permissions:
contents: write
@@ -209,7 +211,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.13.7'
flutter-version: '3.16.9'
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
@@ -230,7 +232,7 @@ jobs:
- name: Upload Artifacts (PR)
if: ${{ github.event_name == 'pull_request' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: feeddeck-macos-universal.zip
path: app/build/macos/Build/Products/Release/feeddeck-macos-universal.zip
@@ -243,10 +245,10 @@ jobs:
upload_url: ${{ github.event.release.upload_url }}
asset_path: app/build/macos/Build/Products/Release/feeddeck-macos-universal.zip
# The "Linux" job builds the Flutter Linux app and uploads it to the GitHub release or the pull request. The job only
# runs for pull requests and when a new release is published.
linux:
name: Linux
# The "Linux (x86_64)" job builds the Flutter Linux app and uploads it to the GitHub release or the pull request. The
# job only runs for pull requests and when a new release is published.
linux-x86_64:
name: Linux (x86_64)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
permissions:
@@ -271,7 +273,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.13.7'
flutter-version: '3.16.9'
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
@@ -297,7 +299,7 @@ jobs:
- name: Upload Artifacts (PR)
if: ${{ github.event_name == 'pull_request' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: feeddeck-linux-x86_64.tar.gz
path: app/build/feeddeck-linux-x86_64.tar.gz
@@ -310,6 +312,77 @@ jobs:
upload_url: ${{ github.event.release.upload_url }}
asset_path: app/build/feeddeck-linux-x86_64.tar.gz
# The "Linux (arm64)" job builds the Flutter Linux app and uploads it to the GitHub release or the pull request. The
# job only runs for pull requests and when a new release is published.
#
# NOTE: Normally this job should run for every pull request and when a new release is published, but since we have to
# pay for the "ubicloud-standard-2-arm" runner, we only run the job when a new release is published.
linux-arm64:
name: Linux (arm64)
runs-on: ubicloud-standard-2-arm
if: github.event_name == 'release' && github.event.action == 'published'
# if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
permissions:
contents: write
defaults:
run:
working-directory: "app"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Packages
run: |
# Required for Flutter
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
# Required for Package "media_kit" which is used via "just_audio_media_kit" for Linux and Windows:
# See: https://pub.dev/packages/media_kit and https://pub.dev/packages/just_audio_media_kit
sudo apt-get install -y libmpv-dev mpv
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.9'
channel: 'master'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
- name: Install Dependencies
run: |
flutter pub get
- name: Build
run: |
flutter config --enable-linux-desktop
flutter build linux --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
- name: Package
run: |
cp linux/flatpak/app.feeddeck.feeddeck.desktop build/linux/arm64/release/bundle/
cp linux/flatpak/app.feeddeck.feeddeck.metainfo.xml build/linux/arm64/release/bundle/
cp linux/flatpak/app.feeddeck.feeddeck.svg build/linux/arm64/release/bundle/
cd build
cp -r linux/arm64/release/bundle/ feeddeck-linux-arm64
tar -czf feeddeck-linux-arm64.tar.gz feeddeck-linux-arm64
- name: Upload Artifacts (PR)
if: ${{ github.event_name == 'pull_request' }}
uses: actions/upload-artifact@v4
with:
name: feeddeck-linux-arm64.tar.gz
path: app/build/feeddeck-linux-arm64.tar.gz
if-no-files-found: error
- name: Upload Artifacts (Release)
uses: shogo82148/actions-upload-release-asset@v1
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: app/build/feeddeck-linux-arm64.tar.gz
# The "Windows" job builds the Flutter Windows app and uploads it to the GitHub release or the pull request. The job
# only runs for pull requests and when a new release is published.
windows:
@@ -329,7 +402,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.13.7'
flutter-version: '3.16.9'
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
@@ -352,7 +425,7 @@ jobs:
- name: Upload Artifacts (PR)
if: ${{ github.event_name == 'pull_request' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: feeddeck-windows-x86_64.zip
path: app/build/feeddeck-windows-x86_64.zip
@@ -364,3 +437,67 @@ jobs:
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: app/build/feeddeck-windows-x86_64.zip
# The "iOS" job builds the Flutter iOS app on every pull request. This is only used to test that the build of the iOS
# app works. The artifact of the build isn't uploaded / used.
ios:
name: iOS
runs-on: macos-14
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
defaults:
run:
working-directory: "app"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.9'
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
- name: Install Dependencies
run: |
flutter pub get
- name: Build
run: |
flutter config --enable-ios
flutter build ipa --no-codesign --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
# The "Android" job builds the Flutter Android app on every pull request. This is only used to test that the build of
# the Android app works. The artifact of the build isn't uploaded / used.
android:
name: Android
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
defaults:
run:
working-directory: "app"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.9'
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
- name: Install Dependencies
run: |
flutter pub get
- name: Build
run: |
flutter config --enable-android
flutter build appbundle --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}

View File

@@ -0,0 +1,53 @@
name: Continuous Integration
on:
push:
branches:
- main
pull_request:
jobs:
flutter:
name: Flutter
runs-on: ubuntu-latest
defaults:
run:
working-directory: "app"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.9'
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
- name: Install Dependencies
run: |
flutter pub get
- name: Test
run: |
flutter test
deno:
name: Deno
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Test
run: |
deno test --allow-env --import-map=supabase/functions/import_map.json supabase/functions

View File

@@ -40,14 +40,14 @@ jobs:
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "16"
node-version: "20"
cache: npm
cache-dependency-path: landing/package-lock.json
- name: Setup Pages
uses: actions/configure-pages@v3
uses: actions/configure-pages@v4
- name: Install Dependencies
run: |
@@ -58,7 +58,7 @@ jobs:
npm run build
- name: Upload Artifact
uses: actions/upload-pages-artifact@v2
uses: actions/upload-pages-artifact@v3
with:
path: ./landing/out
@@ -78,4 +78,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4

7
.gitignore vendored
View File

@@ -1,8 +1,11 @@
# Visual Studio Code Launch Configurations
.vscode/launch.json
# Visual Studio Code
.vscode
# Environment Variables
/supabase/.env.local
/supabase/.env.dev
/supabase/.env.stage
/supabase/.env.prod
# Deno
/coverage_deno

17
.vscode/settings.json vendored
View File

@@ -1,17 +0,0 @@
{
"deno.enable": true,
"deno.unstable": true,
"deno.lint": true,
"deno.enablePaths": [
"./supabase/functions"
],
"deno.importMap": "./supabase/functions/import_map.json",
"editor.formatOnSave": true,
"editor.defaultFormatter": "denoland.vscode-deno",
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
}
}

View File

@@ -57,100 +57,46 @@ check your installed version:
```sh
$ flutter --version
Flutter 3.13.7 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 2f708eb839 (4 days ago) • 2023-10-09 09:58:08 -0500
Engine • revision a794cf2681
Tools • Dart 3.1.3 • DevTools 2.25.0
Flutter 3.16.9 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 41456452f2 (6 days ago) • 2024-01-25 10:06:23 -0800
Engine • revision f40e976bed
Tools • Dart 3.2.6 • DevTools 2.28.5
$ deno --version
deno 1.36.4 (release, aarch64-apple-darwin)
v8 11.6.189.12
typescript 5.1.6
deno 1.40.2 (release, aarch64-apple-darwin)
v8 12.1.285.6
typescript 5.3.3
```
### Working with Flutter
We are recommending to use the
[Visual Studio Code](https://docs.flutter.dev/development/tools/vs-code)
extensions for development.
To run the app you can use the [`run.sh`](./app/run.sh) script, which will
automatically load the `.env` file from the Supabase project and passes the
required variables to the `flutter run` command:
The easiest way to run the Flutter app within Visual Studio Code is to create a
`.vscode/launch.json` file. Within the different configurations you have to
provide the following arguments: `--dart-define SUPABASE_URL=<SUPABASE_URL>`,
`--dart-define SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>`,
`--dart-define SUPABASE_SITE_URL=<SUPABASE_SITE_URL>` and
`--dart-define GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>`.
<details>
<summary>Example: `.vscode/launch.json`</summary>
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Local - Chrome",
"type": "dart",
"request": "launch",
"program": "app/lib/main.dart",
"args": [
"-d",
"chrome",
"--web-port",
"3000",
"--web-browser-flag=--disable-web-security",
"--dart-define",
"SUPABASE_URL=<SUPABASE_URL>",
"--dart-define",
"SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>",
"--dart-define",
"SUPABASE_SITE_URL=<SUPABASE_SITE_URL>",
"--dart-define",
"GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>"
]
},
{
"name": "Local - iOS Simulator",
"type": "dart",
"request": "launch",
"program": "app/lib/main.dart",
"args": [
"-d",
"iPhone 14 Pro Max",
"--dart-define",
"SUPABASE_URL=<SUPABASE_URL>",
"--dart-define",
"SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>",
"--dart-define",
"SUPABASE_SITE_URL=<SUPABASE_SITE_URL>",
"--dart-define",
"GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>"
]
},
{
"name": "Local - macOS",
"type": "dart",
"request": "launch",
"program": "app/lib/main.dart",
"args": [
"-d",
"macOS",
"--dart-define",
"SUPABASE_URL=<SUPABASE_URL>",
"--dart-define",
"SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>",
"--dart-define",
"SUPABASE_SITE_URL=<SUPABASE_SITE_URL>",
"--dart-define",
"GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>"
]
}
]
}
```sh
./run.sh --device="chrome" --environment="local"
```
</details>
To run the tests the following command can be used:
```sh
flutter test
```
To check the test coverage the `--coverage` flag can be added to the command and
an HTML report can be generated:
```sh
flutter test --coverage
# To generate the HTML report lcov is required, which can be installed via Homebrew:
brew install lcov
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
```
#### Sort all Imports
@@ -284,6 +230,38 @@ cd supabase/functions/_cmd
docker-compose up --build
```
To build the Docker image, the following commands can be run:
```sh
docker build -f supabase/functions/_cmd/Dockerfile -t ghcr.io/feeddeck/feeddeck:dev supabase/functions
# To build the Docker image for another platform use the following:
docker buildx build --platform linux/amd64 -f supabase/functions/_cmd/Dockerfile -t ghcr.io/feeddeck/feeddeck:dev supabase/functions
# The Docker image can then be used to run the scheduler, worker or tools, e.g.
docker run ghcr.io/feeddeck/feeddeck:dev tools get-feed '{"type": "reddit", "options": {"reddit": "/r/kubernetes"}}'
```
To run the tests for our code, the following command can be used:
```sh
deno test --allow-env --import-map=supabase/functions/import_map.json supabase/functions
```
To check the test coverage the `--coverage` flag can be added to the command and
an HTML report can be generated:
```sh
deno test --allow-env --import-map=supabase/functions/import_map.json supabase/functions --coverage=coverage_deno
# To generate the HTML report lcov is required, which can be installed via Homebrew:
brew install lcov
deno coverage coverage_deno --lcov --output=coverage_deno/coverage_deno.lcov
genhtml -o coverage_deno/html coverage_deno/coverage_deno.lcov
open coverage_deno/html/index.html
```
## Hosting
FeedDeck uses Supabase as backend. For Supabase we can use
@@ -305,6 +283,7 @@ supabase secrets set --env-file supabase/.env
supabase secrets list
# Deploy all functions
supabase functions deploy add-or-update-source-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
supabase functions deploy add-source-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
supabase functions deploy delete-user-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
supabase functions deploy generate-magic-link-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
@@ -406,21 +385,8 @@ Android, macOS, Windows and Linux if you do not want to use the official ones.
5. Build the app for Web by running `flutter build web`. The build can be found
at `app/build/web` and must be uploaded to your hosting provider.
6. Build the app for Linux by running `flutter build linux --release`. To build
the `arm64` version the following commands can be run on a Raspberry Pi. Once
the `feeddeck-linux-arm64.tar.gz` archive was created it can be uploaded to
the GitHub release.
```sh
cp linux/flatpak/app.feeddeck.feeddeck.desktop build/linux/arm64/release/bundle/
cp linux/flatpak/app.feeddeck.feeddeck.metainfo.xml build/linux/arm64/release/bundle/
cp linux/flatpak/app.feeddeck.feeddeck.svg build/linux/arm64/release/bundle/
cd build
cp -r linux/arm64/release/bundle/ feeddeck-linux-arm64
tar -czf feeddeck-linux-arm64.tar.gz feeddeck-linux-arm64
```
Update the `app.feeddeck.feeddeck.yml` file at
6. Build the app for Linux by running `flutter build linux --release`. Update
the `app.feeddeck.feeddeck.yml` file at
[github.com/flathub/app.feeddeck.feeddeck](https://github.com/flathub/app.feeddeck.feeddeck)
with the new release.

1
app/.gitignore vendored
View File

@@ -31,6 +31,7 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
/coverage
# Symbolication related
app.*.symbols

View File

@@ -49,7 +49,6 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.feeddeck.feeddeck"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
@@ -72,7 +71,12 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
if (keystorePropertiesFile.exists()) {
signingConfig signingConfigs.release
} else {
// For testing purposes we sign with dummy credentials if no key properties are given.
signingConfig signingConfigs.debug
}
}
}
}

View File

@@ -0,0 +1 @@
extensions:

Binary file not shown.

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '11.0'
platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -13,19 +13,25 @@ PODS:
- FMDB/standard (2.7.5)
- just_audio (0.0.1):
- Flutter
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- purchases_flutter (6.0.0):
- purchases_flutter (6.19.0):
- Flutter
- PurchasesHybridCommon (= 9.3.0)
- PurchasesHybridCommon (9.3.0):
- RevenueCat (= 4.33.0)
- RevenueCat (4.33.0)
- screen_brightness_ios (0.1.0):
- Flutter
- PurchasesHybridCommon (= 7.0.0)
- PurchasesHybridCommon (7.0.0):
- RevenueCat (= 4.27.0)
- RevenueCat (4.27.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -36,7 +42,9 @@ PODS:
- FMDB (>= 2.7.5)
- url_launcher_ios (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- volume_controller (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
@@ -46,15 +54,19 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- just_audio (from `.symlinks/plugins/just_audio/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
@@ -75,14 +87,20 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_splash/ios"
just_audio:
:path: ".symlinks/plugins/just_audio/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
purchases_flutter:
:path: ".symlinks/plugins/purchases_flutter/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
@@ -91,29 +109,35 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
media_kit_native_event_loop: f1ee9f941ec0af371b245969a3e010901c375480
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
purchases_flutter: 549ccfbbaf5e7cd195043c714b69a35e278c00f1
PurchasesHybridCommon: af3b2413f9cb999bc1fdca44770bdaf39dfb89fa
RevenueCat: 84fbe2eb9bbf63e1abf346ccd3ff9ee45d633e3b
purchases_flutter: 72d49bdd40138037da9e0cf29a0a355ff0bbed80
PurchasesHybridCommon: 809461dbc8ff23b4dd0d5260c005b4017d6205b6
RevenueCat: 1512a074bebd78b7efb341ce1c33bfc8d292c53a
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
PODFILE CHECKSUM: ec83c31511fbc978a9918c6fda235238118483f5
PODFILE CHECKSUM: 016564c560c4c9dbcb210e12c7aa6039072645f1
COCOAPODS: 1.13.0
COCOAPODS: 1.15.0

View File

@@ -348,7 +348,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -428,7 +428,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -477,7 +477,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View File

@@ -7,10 +7,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:just_audio_background/just_audio_background.dart';
import 'package:media_kit/media_kit.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/repositories/layout_repository.dart';
import 'package:feeddeck/repositories/profile_repository.dart';
import 'package:feeddeck/repositories/settings_repository.dart';
import 'package:feeddeck/utils/constants.dart';
@@ -45,13 +47,23 @@ void main() async {
});
}
/// Initialize the [media_kit] packages, so that we can play audio and video
/// files.
MediaKit.ensureInitialized();
/// Initialize the [just_audio_background] package, so that we can play audio
/// files in the background.
await JustAudioBackground.init(
androidNotificationChannelId: 'com.ryanheise.bg_demo.channel.audio',
androidNotificationChannelName: 'Audio playback',
androidNotificationOngoing: true,
);
///
/// We can not initialize the [just_audio_background] package on Windows and
/// Linux, because then the returned duration in the `_player.durationStream`
/// isn't working correctly in the [ItemAudioPlayer] widget.
if (kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
await JustAudioBackground.init(
androidNotificationChannelId: 'com.ryanheise.bg_demo.channel.audio',
androidNotificationChannelName: 'Audio playback',
androidNotificationOngoing: true,
);
}
/// For the ewb we have to use the path url strategy, so that the redirect
/// within Supabase is working in all cases. On all other platforms this is a
@@ -124,6 +136,7 @@ class FeedDeckApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => LayoutRepository()),
ChangeNotifierProvider(create: (_) => AppRepository()),
ChangeNotifierProvider(create: (_) => ProfileRepository()),
],

View File

@@ -4,14 +4,17 @@ import 'package:feeddeck/models/sources/github.dart';
import 'package:feeddeck/models/sources/googlenews.dart';
import 'package:feeddeck/models/sources/stackoverflow.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/fd_icons.dart';
/// [FDSourceType] is a enum value which defines the source type. A source can
/// have one of the following types:
/// - [github]
/// - [googlenews]
/// - [lemmy]
/// - [mastodon]
/// - [medium]
/// - [nitter]
/// - [pinterest]
/// - [podcast]
/// - [reddit]
/// - [rss]
@@ -27,9 +30,11 @@ import 'package:feeddeck/utils/constants.dart';
enum FDSourceType {
github,
googlenews,
lemmy,
mastodon,
medium,
nitter,
pinterest,
podcast,
reddit,
rss,
@@ -56,12 +61,16 @@ extension FDSourceTypeExtension on FDSourceType {
return 'GitHub';
case FDSourceType.googlenews:
return 'Google News';
case FDSourceType.lemmy:
return 'Lemmy';
case FDSourceType.mastodon:
return 'Mastodon';
case FDSourceType.medium:
return 'Medium';
case FDSourceType.nitter:
return 'Nitter';
case FDSourceType.pinterest:
return 'Pinterest';
case FDSourceType.podcast:
return 'Podcast';
case FDSourceType.reddit:
@@ -81,20 +90,59 @@ extension FDSourceTypeExtension on FDSourceType {
}
}
/// [color] returns the brand color for a source type, which can be used as
/// background color for the icon of a source type.
Color get color {
/// [icon] returns the icon for a source.
IconData get icon {
switch (this) {
case FDSourceType.github:
return FDIcons.github;
case FDSourceType.googlenews:
return FDIcons.googlenews;
case FDSourceType.lemmy:
return FDIcons.lemmy;
case FDSourceType.mastodon:
return FDIcons.mastodon;
case FDSourceType.medium:
return FDIcons.medium;
case FDSourceType.nitter:
return FDIcons.nitter;
case FDSourceType.pinterest:
return FDIcons.pinterest;
case FDSourceType.podcast:
return Icons.podcasts;
case FDSourceType.reddit:
return FDIcons.reddit;
case FDSourceType.rss:
return FDIcons.rss;
case FDSourceType.stackoverflow:
return FDIcons.stackoverflow;
case FDSourceType.tumblr:
return FDIcons.tumblr;
// case FDSourceType.x:
// return FDIcons.x;
case FDSourceType.youtube:
return FDIcons.youtube;
default:
return FDIcons.feeddeck;
}
}
/// [bgColor] returns the background color for the source icon.
Color get bgColor {
switch (this) {
case FDSourceType.github:
return const Color(0xff000000);
case FDSourceType.googlenews:
return const Color(0xff4285f4);
case FDSourceType.lemmy:
return const Color(0xff00bc8c);
case FDSourceType.mastodon:
return const Color(0xff6364ff);
case FDSourceType.medium:
return const Color(0xff000000);
case FDSourceType.nitter:
return const Color(0xffff6c60);
case FDSourceType.pinterest:
return const Color(0xffe60023);
case FDSourceType.podcast:
return const Color(0xff872ec4);
case FDSourceType.reddit:
@@ -113,6 +161,43 @@ extension FDSourceTypeExtension on FDSourceType {
return Constants.primary;
}
}
/// [fgColor] returns the forground color for the source icon. This should be
/// used toether with the [bgColor].
Color get fgColor {
switch (this) {
case FDSourceType.github:
return const Color(0xffffffff);
case FDSourceType.googlenews:
return const Color(0xffffffff);
case FDSourceType.lemmy:
return const Color(0xffffffff);
case FDSourceType.mastodon:
return const Color(0xffffffff);
case FDSourceType.medium:
return const Color(0xffffffff);
case FDSourceType.nitter:
return const Color(0xffffffff);
case FDSourceType.pinterest:
return const Color(0xffffffff);
case FDSourceType.podcast:
return const Color(0xffffffff);
case FDSourceType.reddit:
return const Color(0xffffffff);
case FDSourceType.rss:
return const Color(0xffffffff);
case FDSourceType.stackoverflow:
return const Color(0xffffffff);
case FDSourceType.tumblr:
return const Color(0xffffffff);
// case FDSourceType.x:
// return const Color(0xffffffff);
case FDSourceType.youtube:
return const Color(0xffffffff);
default:
return Constants.onPrimary;
}
}
}
/// [getSourceTypeFromString] returns the [FDSourceType] from his string
@@ -173,7 +258,7 @@ class FDSource {
'id': id,
'type': type.toShortString(),
'title': title,
'options': options != null ? options!.toJson() : null,
'options': options?.toJson(),
'link': link,
'icon': icon,
};
@@ -185,9 +270,11 @@ class FDSource {
class FDSourceOptions {
FDGitHubOptions? github;
FDGoogleNewsOptions? googlenews;
String? lemmy;
String? mastodon;
String? medium;
String? nitter;
String? pinterest;
String? podcast;
String? reddit;
String? rss;
@@ -199,9 +286,11 @@ class FDSourceOptions {
FDSourceOptions({
this.github,
this.googlenews,
this.lemmy,
this.mastodon,
this.medium,
this.nitter,
this.pinterest,
this.podcast,
this.reddit,
this.rss,
@@ -221,6 +310,9 @@ class FDSourceOptions {
responseData['googlenews'] != null
? FDGoogleNewsOptions.fromJson(responseData['googlenews'])
: null,
lemmy: responseData.containsKey('lemmy') && responseData['lemmy'] != null
? responseData['lemmy']
: null,
mastodon: responseData.containsKey('mastodon') &&
responseData['mastodon'] != null
? responseData['mastodon']
@@ -233,6 +325,10 @@ class FDSourceOptions {
responseData.containsKey('nitter') && responseData['nitter'] != null
? responseData['nitter']
: null,
pinterest: responseData.containsKey('pinterest') &&
responseData['pinterest'] != null
? responseData['pinterest']
: null,
podcast:
responseData.containsKey('podcast') && responseData['podcast'] != null
? responseData['podcast']
@@ -266,9 +362,11 @@ class FDSourceOptions {
return {
'github': github?.toJson(),
'googlenews': googlenews?.toJson(),
'lemmy': lemmy,
'mastodon': mastodon,
'medium': medium,
'nitter': nitter,
'pinterest': pinterest,
'podcast': podcast,
'reddit': reddit,
'rss': rss,

View File

@@ -376,21 +376,33 @@ class AppRepository with ChangeNotifier {
}
/// [addSource] is called to add a source to the column with the provided
/// [columnId]. The function takes a [source] as parameter. The function calls
/// the `add-source-v1` edge function via the Supabase client to create the
/// source. When the source was created the newly returned source is added to
/// the list of sources of the column with the provided [columnId].
/// [columnId]. Next to [columnId] a user must also provide the [type] and
/// [options] for the source. The function calls the `add-or-update-source-v1`
/// edge function via the Supabase client to create the source. When the
/// source was created the newly returned source is added to the list of
/// sources of the column with the provided [columnId].
///
/// The optional [feedData] parameter is used to provide the feed data for the
/// source. This is can be used to scrape the source data via the client (app)
/// instead of the server (scheduler / worker).
Future<void> addSource(
String columnId,
FDSourceType type,
FDSourceOptions options,
) async {
FDSourceOptions options, [
String? feedData,
]) async {
final result = await Supabase.instance.client.functions.invoke(
'add-source-v1',
'add-or-update-source-v1',
body: {
'columnId': columnId,
'type': type.toShortString(),
'options': options.toJson(),
'source': {
'id': '',
'columnId': columnId,
'userId': '',
'type': type.toShortString(),
'title': '',
'options': options.toJson(),
},
'feedData': feedData,
},
);

View File

@@ -141,7 +141,7 @@ class ItemsRepository with ChangeNotifier {
/// selected source which is stored in the [_filters.sourceIdFilter]
/// field.
if (_filters.sourceIdFilter != '') {
filter = filter.eq('sourceId', sourceIdFilter);
filter = filter.eq('sourceId', _filters.sourceIdFilter);
}
filter = filter.lte('createdAt', _filters.createdAtFilter);
@@ -305,7 +305,7 @@ class ItemsRepository with ChangeNotifier {
for (var i = 0; i < chunks.length; i++) {
await Supabase.instance.client
.from('items')
.update({'isRead': read}).in_('id', chunks[i]);
.update({'isRead': read}).inFilter('id', chunks[i]);
for (var j = 0; j < _items.length; j++) {
if (chunks[i].contains(_items[j].id)) {
_items[j].isRead = read;

View File

@@ -0,0 +1,12 @@
import 'package:flutter/foundation.dart';
/// The [LayoutRepository] is used to store several layout information of the
/// app, which can be modifed or should be modifed within different locations.
class LayoutRepository with ChangeNotifier {
/// [_deckLayoutSmallInitialTabIndex] stores the selected tab index of the
/// [DeckLayoutSmall] widget. This is used that we display the same tab when
/// a user switches between the small and large layout (e.g. portrait and
/// landscape mode on mobile devices) and that we can reset the tab index when
/// a user selects a new deck.
int deckLayoutSmallInitialTabIndex = 0;
}

View File

@@ -56,15 +56,15 @@ const _htmlAuthFinished = '''
</html>
''';
/// The [DesktopLoginManager] is used to authenticate a user with the provided
/// The [DesktopSignInManager] is used to authenticate a user with the provided
/// OAuth [provider] on desktop platforms.
class DesktopLoginManager {
final supabase.Provider provider;
class DesktopSignInManager {
final supabase.OAuthProvider provider;
final Map<String, String>? queryParams;
HttpServer? redirectServer;
DesktopLoginManager({
DesktopSignInManager({
required this.provider,
required this.queryParams,
});

View File

@@ -1,4 +1,6 @@
/// Flutter icons FDIcons
import 'package:flutter/widgets.dart';
/// Flutter icons [FDIcons]
/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
@@ -11,11 +13,6 @@
/// fonts:
/// - asset: fonts/FDIcons.ttf
///
///
///
import 'package:flutter/widgets.dart';
class FDIcons {
FDIcons._();
@@ -32,6 +29,8 @@ class FDIcons {
IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData googlenews =
IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData pinterest =
IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData medium =
IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData nitter =
@@ -64,4 +63,6 @@ class FDIcons {
IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData mastodon =
IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData lemmy =
IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View File

@@ -0,0 +1,55 @@
import 'package:http/http.dart' as http;
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/utils/api_exception.dart';
/// [getFeed] returns the feed for the provided [sourceType] and [options]. It
/// can be used to fetch the feed for a source on the client side (app) instead
/// of via the corresponding `add-or-update-source-v1` edge function or via our
/// worker.
///
/// The functions for the different sources must implement the same parsing for
/// the source options as it is done in the edge function.
Future<String> getFeed(FDSourceType sourceType, FDSourceOptions options) async {
switch (sourceType) {
case FDSourceType.reddit:
return getFeedReddit(options.reddit);
default:
throw const ApiException('Unknown source type', 400);
}
}
/// [getFeedReddit] returns the feed for the provided [input]. It is used to
/// fetch the RSS feed for a Reddit source, which can be passed to the
/// `add-or-update-source-v1` edge function.
///
/// The function must implement the same parsing logic as it is done in the
/// `supabase/functions/_shared/feed/reddit.ts` file.
Future<String> getFeedReddit(String? input) async {
if (input == null || input.isEmpty) {
throw const ApiException('No input provided', 400);
}
String url = '';
try {
if (input.startsWith('/r/') || input.startsWith('/u/')) {
url = 'https://www.reddit.com$input.rss';
} else {
final inputUri = Uri.parse(input);
if (inputUri.host.endsWith('reddit.com')) {
if (input.endsWith('.rss')) {
url = input;
} else {
url = '$input.rss';
}
} else {
throw const ApiException('Invalid input', 400);
}
}
} catch (err) {
throw const ApiException('Invalid input', 400);
}
final response = await http.get(Uri.parse(url));
return response.body;
}

View File

@@ -1,32 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:feeddeck/repositories/settings_repository.dart';
/// [FDImageType] is a enum value which defines the image type. An image can be
/// related to an item or a source.
enum FDImageType {
item,
source,
}
/// [getImageUrl] returns the correct image url to use for the provided image
/// url:
/// - If the [imageType] is [FDImageType.source] the image is always requested
/// from the Supabase storage.
/// - If the [imageType] is [FDImageType.item] and the app runs on the web, the
/// image is proxied through the Supabase functions.
/// - If the [imageType] is [FDImageType.item] and the app runs on a mobile or
/// desktop device, the image is requested directly from the provided url.
String getImageUrl(FDImageType imageType, String imageUrl) {
if (imageType == FDImageType.source) {
return '${SettingsRepository().supabaseUrl}/storage/v1/object/public/sources/$imageUrl';
}
if (kIsWeb) {
return '${Supabase.instance.client.functionsUrl}/image-proxy-v1?media=${Uri.encodeQueryComponent(imageUrl)}';
}
return imageUrl;
}

View File

@@ -0,0 +1,33 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
/// [signInWithApple] performs Apple sign in on iOS and macOS.
/// See https://supabase.com/docs/guides/auth/social-login/auth-apple?platform=flutter#using-native-sign-in-with-apple-in-flutter
Future<AuthResponse> signInWithApple() async {
final rawNonce = Supabase.instance.client.auth.generateRawNonce();
final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: hashedNonce,
);
final idToken = credential.identityToken;
if (idToken == null) {
throw const AuthException(
'Could not find ID Token from generated credential.',
);
}
return Supabase.instance.client.auth.signInWithIdToken(
provider: OAuthProvider.apple,
idToken: idToken,
nonce: rawNonce,
);
}

View File

@@ -5,6 +5,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/repositories/layout_repository.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/widgets/column/column_layout.dart';
import 'package:feeddeck/widgets/column/create/create_column.dart';
@@ -224,10 +225,20 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
backgroundColor: Constants.background,
selectedIndex: null,
/// When a user selects a destination in the navigation rail
/// we scroll to the corresponding column by using the
/// `scroll_to_index` package.
/// When a user selects a destination in the navigation
/// rail we scroll to the corresponding column by using
/// the `scroll_to_index` package.
onDestinationSelected: (int index) {
/// Before we scroll to the corresponding column, we
/// also update the [deckLayoutSmallInitialTabIndex] in
/// the [LayoutRepository] so that the correct tab is
/// also selected when a user switches to the small
/// layout.
Provider.of<LayoutRepository>(
context,
listen: false,
).deckLayoutSmallInitialTabIndex = index;
_scrollController.scrollToIndex(
index,
preferPosition: AutoScrollPosition.end,
@@ -244,8 +255,8 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
/// We add two additional items to the navigation rail via
/// the trailing property. These items are used to allow a
/// user to create a new column and to go to the settings of
/// the app.
/// user to create a new column and to go to the settings
/// of the app.
trailing: Expanded(
child: Align(
alignment: Alignment.bottomCenter,

View File

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/repositories/layout_repository.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/widgets/column/column_layout.dart';
import 'package:feeddeck/widgets/column/create/create_column.dart';
@@ -20,6 +21,30 @@ import 'package:feeddeck/widgets/source/source_icon.dart';
class DeckLayoutSmall extends StatelessWidget {
const DeckLayoutSmall({super.key});
/// [_getInitialIndex] returns the initial index for the
/// [DefaultTabController]. The intial index is saved in the
/// [LayoutRepository] so that we can use it when a user switches between the
/// small and large layout or between decks. To not run into errors we have to
/// check that a column which should be used was not already deleted by a
/// user before returning the index. If a column was deleted we reset the
/// index to 0.
int _getInitialIndex(BuildContext context, int columnsLength) {
final deckLayoutSmallInitialTabIndex = Provider.of<LayoutRepository>(
context,
listen: false,
).deckLayoutSmallInitialTabIndex;
if (deckLayoutSmallInitialTabIndex >= columnsLength) {
Provider.of<LayoutRepository>(
context,
listen: false,
).deckLayoutSmallInitialTabIndex = 0;
return 0;
}
return deckLayoutSmallInitialTabIndex;
}
/// [_buildTabs] returns all items for the tab bar. The items are generated
/// by looping thorugh the `columns` defined in the [AppRepository].
///
@@ -124,6 +149,8 @@ class DeckLayoutSmall extends StatelessWidget {
AppRepository app = Provider.of<AppRepository>(context, listen: true);
return DefaultTabController(
key: ValueKey(app.activeDeckId),
initialIndex: _getInitialIndex(context, app.columns.length),
length: app.columns.length,
child: Scaffold(
bottomNavigationBar: SafeArea(
@@ -148,6 +175,17 @@ class DeckLayoutSmall extends StatelessWidget {
),
child: TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
onTap: (int index) {
/// When the user clicks on a tab we update the index in
/// the [LayoutRepository] so that we can use it as
/// initial index when the widget is rebuild (e.g. when
/// a user switches between the large and small layout).
Provider.of<LayoutRepository>(
context,
listen: false,
).deckLayoutSmallInitialTabIndex = index;
},
tabs: _buildTabs(context),
),
),

View File

@@ -24,7 +24,7 @@ import 'package:flutter/material.dart';
/// ),
/// )
class ElevatedButtonProgressIndicator extends StatelessWidget {
const ElevatedButtonProgressIndicator({Key? key}) : super(key: key);
const ElevatedButtonProgressIndicator({super.key});
@override
Widget build(BuildContext context) {

View File

@@ -7,9 +7,11 @@ import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/items_repository.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/openurl.dart';
import 'package:feeddeck/widgets/item/details/item_details_lemmy.dart';
import 'package:feeddeck/widgets/item/details/item_details_mastodon.dart';
import 'package:feeddeck/widgets/item/details/item_details_medium.dart';
import 'package:feeddeck/widgets/item/details/item_details_nitter.dart';
import 'package:feeddeck/widgets/item/details/item_details_pinterest.dart';
import 'package:feeddeck/widgets/item/details/item_details_podcast.dart';
import 'package:feeddeck/widgets/item/details/item_details_reddit.dart';
import 'package:feeddeck/widgets/item/details/item_details_rss.dart';
@@ -70,6 +72,11 @@ class ItemDetails extends StatelessWidget {
/// corresponding preview item.
case FDSourceType.googlenews:
return Container();
case FDSourceType.lemmy:
return ItemDetailsLemmy(
item: item,
source: source,
);
case FDSourceType.mastodon:
return ItemDetailsMastodon(
item: item,
@@ -85,6 +92,11 @@ class ItemDetails extends StatelessWidget {
item: item,
source: source,
);
case FDSourceType.pinterest:
return ItemDetailsPinterest(
item: item,
source: source,
);
case FDSourceType.podcast:
return ItemDetailsPodcast(
item: item,
@@ -196,7 +208,7 @@ class ItemDetails extends StatelessWidget {
Constants.elevatedButtonSize,
),
),
label: const Text('Open link'),
label: const Text('Open Link'),
onPressed: () => _openUrl(item.link),
icon: const Icon(Icons.launch),
),

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
class ItemDetailsLemmy extends StatelessWidget {
const ItemDetailsLemmy({
super.key,
required this.item,
required this.source,
});
final FDItem item;
final FDSource source;
/// [_buildMedia] builds the media widget for the item. The media widget can
/// display an image, a video or y YouTube video.
///
/// See the `getMedia` function in the `lemmy.ts` file, for a list of
/// extension which are a image / video.
Widget _buildMedia() {
if (item.media != null && item.media! != '') {
final mediaUrl = Uri.parse(item.media!);
if (mediaUrl.path.endsWith('.jpg') ||
mediaUrl.path.endsWith('.jpeg') ||
mediaUrl.path.endsWith('.png') ||
mediaUrl.path.endsWith('.gif')) {
return ItemMedia(
itemMedia: item.media,
);
}
if (mediaUrl.path.endsWith('.mp4')) {
return ItemVideoPlayer(
video: item.media!,
);
}
if (item.media!.startsWith('https://youtu.be/') ||
item.media!.startsWith('https://www.youtube.com/watch?') ||
item.media!.startsWith('https://m.youtube.com/watch?')) {
return ItemYoutubeVideo(
null,
item.media!,
);
}
}
return Container();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ItemTitle(
itemTitle: item.title,
),
ItemSubtitle(
item: item,
source: source,
),
_buildMedia(),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
),
],
);
}
}

View File

@@ -6,6 +6,8 @@ import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media_gallery.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
class ItemDetailsMastodon extends StatelessWidget {
const ItemDetailsMastodon({
@@ -17,6 +19,80 @@ class ItemDetailsMastodon extends StatelessWidget {
final FDItem item;
final FDSource source;
/// [_getYoutubeUrl] returns a YouTube url when the provided [description]
/// contains a YouTube link. If the [description] does not contain a YouTube
/// link, the function returns `null`.
String? _getYoutubeUrl(String description) {
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
final matches = exp.allMatches(description);
for (var match in matches) {
final url = description.substring(match.start, match.end);
if (url.startsWith('https://youtu.be/') ||
url.startsWith('https://www.youtube.com/watch?') ||
url.startsWith('https://m.youtube.com/watch?')) {
return url;
}
}
return null;
}
/// [_buildDescription] builds the description widget for the item. If the
/// description contains a YouTube link, we render the [ItemYoutubeVideo]
/// and the [ItemDescription] widgets. If the description does not contain a
/// YouTube link, we render the [ItemDescription], [ItemMediaGallery] and
/// [ItemVideos] widget.
List<Widget> _buildDescription() {
final youtubeUrl =
item.description != null ? _getYoutubeUrl(item.description!) : null;
if (youtubeUrl != null) {
return [
ItemYoutubeVideo(
item.media,
youtubeUrl,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
disableImages: true,
),
];
}
return [
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
disableImages: true,
),
const SizedBox(
height: Constants.spacingExtraSmall,
),
ItemMediaGallery(
itemMedias: item.options != null &&
item.options!.containsKey('media') &&
item.options!['media'] != null
? (item.options!['media'] as List)
.map((item) => item as String)
.toList()
: null,
),
ItemVideos(
videos: item.options != null &&
item.options!.containsKey('videos') &&
item.options!['videos'] != null
? (item.options!['videos'] as List)
.map((item) => item as String)
.toList()
: null,
),
];
}
@override
Widget build(BuildContext context) {
return Column(
@@ -27,21 +103,7 @@ class ItemDetailsMastodon extends StatelessWidget {
item: item,
source: source,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
),
const SizedBox(
height: Constants.spacingExtraSmall,
),
ItemMediaGallery(
itemMedias: item.options != null && item.options!.containsKey('media')
? (item.options!['media'] as List)
.map((item) => item as String)
.toList()
: null,
),
..._buildDescription(),
],
);
}

View File

@@ -5,8 +5,8 @@ import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media_gallery.dart';
import 'package:feeddeck/widgets/item/details/utils/item_piped/item_piped_video.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
class ItemDetailsNitter extends StatelessWidget {
const ItemDetailsNitter({
@@ -18,18 +18,37 @@ class ItemDetailsNitter extends StatelessWidget {
final FDItem item;
final FDSource source;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ItemTitle(
itemTitle: item.title,
),
ItemSubtitle(
item: item,
source: source,
/// [_getPipedUrl] returns a Piped url when the provided [description]
/// contains a Piped link. If the [description] does not contain a Piped link,
/// the function returns `null`.
String? _getPipedUrl(String description) {
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
final matches = exp.allMatches(description);
for (var match in matches) {
final url = description.substring(match.start, match.end);
if (url.startsWith('https://piped.video/watch?v=') ||
url.startsWith('https://piped.video/')) {
return url;
}
}
return null;
}
/// [_buildDescription] builds the description widget for the item. If the
/// description contains a Piped link, we render the [ItemPipedVideo] and the
/// [ItemDescription] widgets. If the description does not contain a Piped
/// link, we render the [ItemDescription] and [ItemMediaGallery] widget.
List<Widget> _buildDescription() {
final pipedUrl =
item.description != null ? _getPipedUrl(item.description!) : null;
if (pipedUrl != null) {
return [
ItemPipedVideo(
item.media,
pipedUrl,
),
ItemDescription(
itemDescription: item.description,
@@ -37,16 +56,40 @@ class ItemDetailsNitter extends StatelessWidget {
tagetFormat: DescriptionFormat.markdown,
disableImages: true,
),
const SizedBox(
height: Constants.spacingExtraSmall,
),
ItemMediaGallery(
itemMedias: item.options != null && item.options!.containsKey('media')
? (item.options!['media'] as List)
.map((item) => item as String)
.toList()
: null,
];
}
return [
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
disableImages: true,
),
const SizedBox(
height: Constants.spacingExtraSmall,
),
ItemMediaGallery(
itemMedias: item.options != null && item.options!.containsKey('media')
? (item.options!['media'] as List)
.map((item) => item as String)
.toList()
: null,
),
];
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ItemSubtitle(
item: item,
source: source,
),
..._buildDescription(),
],
);
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
class ItemDetailsPinterest extends StatelessWidget {
const ItemDetailsPinterest({
super.key,
required this.item,
required this.source,
});
final FDItem item;
final FDSource source;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ItemTitle(
itemTitle: item.title,
),
ItemSubtitle(
item: item,
source: source,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
),
],
);
}
}

View File

@@ -1,15 +1,13 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/image_url.dart';
import 'package:feeddeck/widgets/item/details/utils/item_audio_palyer/item_audio_player.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
class ItemDetailsPodcast extends StatelessWidget {
const ItemDetailsPodcast({
@@ -21,14 +19,6 @@ class ItemDetailsPodcast extends StatelessWidget {
final FDItem item;
final FDSource source;
String? _buildImageUrl() {
if (source.icon != null && source.icon != '') {
return getImageUrl(FDImageType.source, source.icon!);
}
return null;
}
/// [_buildImage] returns an image which is displayed above the
/// [ItemAudioPlayer]. For this image we are using the [source.icon] if it is
/// available. If the [source.icon] is not available, we are not displaying
@@ -55,8 +45,6 @@ class ItemDetailsPodcast extends StatelessWidget {
@override
Widget build(BuildContext context) {
final imageUrl = _buildImageUrl();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
@@ -74,7 +62,7 @@ class ItemDetailsPodcast extends StatelessWidget {
/// directly listen to the podcast episode.
Column(
children: [
_buildImage(imageUrl),
_buildImage(source.icon),
const SizedBox(
height: Constants.spacingMiddle,
),
@@ -83,7 +71,7 @@ class ItemDetailsPodcast extends StatelessWidget {
audioFile: item.media!,
audioTitle: item.title,
audioArtist: source.title,
audioArt: imageUrl,
audioArt: getImageUrl(source.icon!),
),
const SizedBox(
height: Constants.spacingMiddle,

View File

@@ -5,6 +5,7 @@ import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
class ItemDetailsReddit extends StatelessWidget {
const ItemDetailsReddit({
@@ -16,6 +17,60 @@ class ItemDetailsReddit extends StatelessWidget {
final FDItem item;
final FDSource source;
/// [_getYoutubeUrl] returns a YouTube url when the provided [description]
/// contains a YouTube link. If the [description] does not contain a YouTube
/// link, the function returns `null`.
String? _getYoutubeUrl(String description) {
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
final matches = exp.allMatches(description);
for (var match in matches) {
final url = description.substring(match.start, match.end);
if (url.startsWith('https://youtu.be/') ||
url.startsWith('https://www.youtube.com/watch?') ||
url.startsWith('https://m.youtube.com/watch?')) {
return url;
}
}
return null;
}
/// [_buildDescription] builds the description widget for the item. If the
/// description contains a YouTube link, we render the [ItemYoutubeVideo]
/// and the [ItemDescription] widgets. If the description does not contain a
/// YouTube link, we only render the [ItemDescription] widget.
///
/// If the description containes a YouTube link we also have to disable the
/// rendering of images within the [ItemDescription] widget.
List<Widget> _buildDescription() {
final youtubeUrl =
item.description != null ? _getYoutubeUrl(item.description!) : null;
if (youtubeUrl != null) {
return [
ItemYoutubeVideo(
item.media,
youtubeUrl,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
disableImages: true,
),
];
}
return [
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
),
];
}
@override
Widget build(BuildContext context) {
return Column(
@@ -29,11 +84,7 @@ class ItemDetailsReddit extends StatelessWidget {
item: item,
source: source,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
),
..._buildDescription(),
],
);
}

View File

@@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:html/parser.dart' show parse;
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
class ItemDetailsRSS extends StatelessWidget {
const ItemDetailsRSS({
@@ -17,6 +20,31 @@ class ItemDetailsRSS extends StatelessWidget {
final FDItem item;
final FDSource source;
/// [_buildMedia] renders an image or video for the item. If the description
/// of the item contains an image we do not render the image, because it could
/// already be rendered via the description.
///
/// Videos are currently always rendered, because they will not be rendered,
/// by the [MarkdownBody] widget.
Widget _buildMedia() {
if (item.options != null &&
item.options!.containsKey('video') &&
item.options!['video'] != null) {
return ItemVideos(videos: [item.options!['video']]);
}
/// Check if the description of the RSS feed contains an image. If this is
/// the case we do not render the image from the [item.media] because the
/// image is already rendered in the [ItemDescription] widget.
if (parse(item.description).querySelectorAll('img').isNotEmpty) {
return Container();
}
return ItemMedia(
itemMedia: item.media,
);
}
@override
Widget build(BuildContext context) {
return Column(
@@ -30,13 +58,11 @@ class ItemDetailsRSS extends StatelessWidget {
item: item,
source: source,
),
ItemMedia(
itemMedia: item.media,
),
_buildMedia(),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.plain,
tagetFormat: DescriptionFormat.plain,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
),
],
);

View File

@@ -1,17 +1,11 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:youtube_player_iframe/youtube_player_iframe.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
class ItemDetailsYoutube extends StatelessWidget {
const ItemDetailsYoutube({
@@ -23,22 +17,6 @@ class ItemDetailsYoutube extends StatelessWidget {
final FDItem item;
final FDSource source;
/// [_buildMedia] returns the media element for the item. On the web we are
/// using the [YoutubeVideo] widget to render the video, so that a user can
/// directly play the YouTube video. On all other platforms we display the
/// thumbnail of the video.
Widget _buildMedia() {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
return YoutubeVideo(
videoUrl: item.link,
);
}
return ItemMedia(
itemMedia: item.media,
);
}
@override
Widget build(BuildContext context) {
return Column(
@@ -52,7 +30,10 @@ class ItemDetailsYoutube extends StatelessWidget {
item: item,
source: source,
),
_buildMedia(),
ItemYoutubeVideo(
item.media,
item.link,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.plain,
@@ -62,51 +43,3 @@ class ItemDetailsYoutube extends StatelessWidget {
);
}
}
class YoutubeVideo extends StatefulWidget {
const YoutubeVideo({
super.key,
required this.videoUrl,
});
final String videoUrl;
@override
State<YoutubeVideo> createState() => _YoutubeVideoState();
}
class _YoutubeVideoState extends State<YoutubeVideo> {
late YoutubePlayerController _controller;
@override
void initState() {
super.initState();
_controller = YoutubePlayerController(
params: const YoutubePlayerParams(
showControls: true,
showFullscreenButton: false,
),
);
_controller.cueVideoByUrl(mediaContentUrl: widget.videoUrl);
_controller.cueVideoById(
videoId: widget.videoUrl.replaceFirst(
'https://www.youtube.com/watch?v=',
'',
),
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(
bottom: Constants.spacingMiddle,
),
child: YoutubePlayer(
controller: _controller,
aspectRatio: 16 / 9,
),
);
}
}

View File

@@ -65,6 +65,13 @@ class _ItemAudioPlayerState extends State<ItemAudioPlayer> {
@override
void dispose() {
/// We have to dispose the [_player] when the widget is disposed, otherwise
/// the audio will continue to play in the background.
///
/// On Linux and Windows the audio will continue to play even if the
/// [_player] is disposed, so that we also call the `pause` method of the
/// [_player] to stop the audio.
_player.pause();
_player.dispose();
super.dispose();
}

View File

@@ -1,14 +1,12 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:html2md/html2md.dart' as html2md;
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/font.dart';
import 'package:feeddeck/utils/openurl.dart';
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
/// The [DescriptionFormat] enum defines the source and target format of a
/// description.
@@ -47,7 +45,7 @@ class ItemDescription extends StatelessWidget {
Widget _buildMarkdown(BuildContext context, String content) {
return MarkdownBody(
selectable: true,
data: content,
data: content.trim(),
styleSheet: MarkdownStyleSheet(
code: TextStyle(
fontFamily: getMonospaceFontFamily(),
@@ -67,12 +65,6 @@ class ItemDescription extends StatelessWidget {
return Container();
}
String imageUrl = uri.toString();
if (kIsWeb) {
imageUrl =
'${Supabase.instance.client.functionsUrl}/image-proxy-v1?media=${Uri.encodeQueryComponent(imageUrl)}';
}
return Container(
padding: const EdgeInsets.only(
bottom: Constants.spacingMiddle,
@@ -87,6 +79,9 @@ class ItemDescription extends StatelessWidget {
isDismissible: true,
useSafeArea: true,
backgroundColor: Colors.black,
constraints: const BoxConstraints(
maxWidth: double.infinity,
),
builder: (BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
@@ -95,7 +90,7 @@ class ItemDescription extends StatelessWidget {
Center(
child: CachedNetworkImage(
fit: BoxFit.contain,
imageUrl: imageUrl,
imageUrl: uri.toString(),
placeholder: (context, url) => Container(),
errorWidget: (context, url, error) => Container(),
),
@@ -121,7 +116,7 @@ class ItemDescription extends StatelessWidget {
child: CachedNetworkImage(
width: double.infinity,
fit: BoxFit.contain,
imageUrl: imageUrl,
imageUrl: uri.toString(),
placeholder: (context, url) => Container(),
errorWidget: (context, url, error) => Container(),
),
@@ -135,7 +130,7 @@ class ItemDescription extends StatelessWidget {
/// [_buildPlain] renders the provided [content] as plain text.
Widget _buildPlain(String content) {
return SelectableText(
content,
content.trim(),
textAlign: TextAlign.left,
style: const TextStyle(
fontWeight: FontWeight.normal,
@@ -163,7 +158,9 @@ class ItemDescription extends StatelessWidget {
if (sourceFormat == DescriptionFormat.html &&
tagetFormat == DescriptionFormat.plain) {
return _buildPlain(
itemDescription!.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ''),
itemDescription!
.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ')
.replaceAll(RegExp('\\s+'), ' '),
);
}

View File

@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/image_url.dart';
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
/// The [ItemMedia] widget displays the media of an item. Based on the provided
/// [itemMedia] value the media is displayed from the Supabase storage or
@@ -22,8 +20,6 @@ class ItemMedia extends StatelessWidget {
return Container();
}
final imageUrl = getImageUrl(FDImageType.item, itemMedia!);
return Container(
padding: const EdgeInsets.only(
bottom: Constants.spacingMiddle,
@@ -38,6 +34,9 @@ class ItemMedia extends StatelessWidget {
isDismissible: true,
useSafeArea: true,
backgroundColor: Colors.black,
constraints: const BoxConstraints(
maxWidth: double.infinity,
),
builder: (BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
@@ -46,7 +45,7 @@ class ItemMedia extends StatelessWidget {
Center(
child: CachedNetworkImage(
fit: BoxFit.contain,
imageUrl: imageUrl,
imageUrl: itemMedia!,
placeholder: (context, url) => Container(),
errorWidget: (context, url, error) => Container(),
),
@@ -72,7 +71,7 @@ class ItemMedia extends StatelessWidget {
child: CachedNetworkImage(
width: double.infinity,
fit: BoxFit.contain,
imageUrl: imageUrl,
imageUrl: itemMedia!,
placeholder: (context, url) => Container(),
errorWidget: (context, url, error) => Container(),
),

View File

@@ -1,15 +1,14 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/image_url.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
/// The [ItemMediaGallery] widget can be used to display multiple media files in
/// a gallery. Similar to the [ItemMedia] widget the provided [itemMedias]
/// values can be displayed from the Supabase storage or directly from the
/// a gallery. Similar to the [ItemMedia] widget the provided [itemMedias]
/// provided url.
class ItemMediaGallery extends StatelessWidget {
const ItemMediaGallery({
@@ -22,8 +21,6 @@ class ItemMediaGallery extends StatelessWidget {
/// [_buildSingleMedia] displays a single media file in the gallery. If the
/// app runs on the web, we proxy the url through the Supabase functions.
Widget _buildSingleMedia(BuildContext context, int itemMediaIndex) {
final imageUrl = getImageUrl(FDImageType.item, itemMedias![itemMediaIndex]);
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
@@ -34,6 +31,9 @@ class ItemMediaGallery extends StatelessWidget {
isDismissible: true,
useSafeArea: true,
backgroundColor: Colors.black,
constraints: const BoxConstraints(
maxWidth: double.infinity,
),
builder: (BuildContext context) {
return ItemMediaGalleryModal(
initialItemMediaIndex: itemMediaIndex,
@@ -45,7 +45,7 @@ class ItemMediaGallery extends StatelessWidget {
child: CachedNetworkImage(
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
imageUrl: imageUrl,
imageUrl: itemMedias![itemMediaIndex],
placeholder: (context, url) => Container(),
errorWidget: (context, url, error) => Container(),
),
@@ -179,7 +179,7 @@ class ItemMediaGalleryModal extends StatelessWidget {
(itemMedia) => Center(
child: CachedNetworkImage(
fit: BoxFit.contain,
imageUrl: getImageUrl(FDImageType.item, itemMedia),
imageUrl: itemMedia,
placeholder: (context, url) => Container(),
errorWidget: (context, url, error) => Container(),
),

View File

@@ -0,0 +1,20 @@
library item_piped_video;
import 'package:flutter/material.dart';
import 'item_piped_video_stub.dart'
if (dart.library.io) 'item_piped_video_native.dart'
if (dart.library.html) 'item_piped_video_web.dart';
/// The [ItemPipedVideo] class implements a widget that displays a video from
/// Piped.
///
/// This is required because we are using different implementations for the web
/// and for all other target platforms (Android, iOS, macOS, Windows, Linux). On
/// the web we display the Piped video via an `iframe` element. On all other
/// platforms we are using the [piped_client] package to fetch the url of the
/// Piped video, which can then be displayed via our [ItemVideoPlayer] widget.
abstract class ItemPipedVideo implements StatefulWidget {
factory ItemPipedVideo(String? imageUrl, String videoUrl) =>
getItemPipedVideo(imageUrl, videoUrl);
}

View File

@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:piped_client/piped_client.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
import 'item_piped_video.dart';
/// The [ItemVideoQuality] class represents a list of video qualities for the
/// requested Piped video and the corresponding audio stream.
class ItemVideoQualitiesAndAudio {
const ItemVideoQualitiesAndAudio({
required this.qualities,
required this.audio,
});
final List<ItemVideoQuality> qualities;
final String audio;
}
/// [_getVideoId] returns the id of the provide video url, which can be used to
/// get the video streams via the Piped API.
String _getVideoId(String videoUrl) {
if (videoUrl.startsWith('https://piped.video/watch?v=')) {
return videoUrl.replaceFirst(
'https://piped.video/watch?v=',
'',
);
}
if (videoUrl.startsWith('https://piped.video/')) {
return videoUrl.replaceFirst(
'https://piped.video/',
'',
);
}
return videoUrl;
}
class ItemPipedVideoNative extends StatefulWidget implements ItemPipedVideo {
const ItemPipedVideoNative({
super.key,
required this.imageUrl,
required this.videoUrl,
});
final String? imageUrl;
final String videoUrl;
@override
State<ItemPipedVideoNative> createState() => _ItemPipedVideoNativeState();
}
class _ItemPipedVideoNativeState extends State<ItemPipedVideoNative> {
final piped = PipedClient();
late Future<ItemVideoQualitiesAndAudio> _futureFetchVideoAndAudioUrls;
/// [_fetchVideoAndAudioUrls] fetches the video and audio urls for the
/// requested Piped video. Since the video streams do not contain the audio
/// stream, we have to fetch the audio stream separately.
Future<ItemVideoQualitiesAndAudio> _fetchVideoAndAudioUrls() async {
final streams = await piped.streams(_getVideoId(widget.videoUrl));
return ItemVideoQualitiesAndAudio(
qualities: streams.videoStreams
.where(
(element) =>
element.mimeType == 'video/mp4' &&
element.format == PipedVideoStreamFormat.mp4,
)
.map(
(element) => ItemVideoQuality(
quality: element.quality,
video: element.url,
),
)
.toList(),
audio: streams.audioStreams
.where((element) => element.mimeType == 'audio/mp4')
.map((element) => element.url)
.first,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
setState(() {
_futureFetchVideoAndAudioUrls = _fetchVideoAndAudioUrls();
});
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _futureFetchVideoAndAudioUrls,
builder: (
BuildContext context,
AsyncSnapshot<ItemVideoQualitiesAndAudio> snapshot,
) {
if (snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasError ||
snapshot.data == null ||
snapshot.data!.qualities.isEmpty ||
snapshot.data!.audio.isEmpty) {
return ItemMedia(itemMedia: widget.imageUrl);
}
return ItemVideoPlayer(
video: snapshot.data!.qualities.first.video,
audio: snapshot.data!.audio,
qualities: snapshot.data!.qualities,
);
},
);
}
}
ItemPipedVideo getItemPipedVideo(String? imageUrl, String videoUrl) =>
ItemPipedVideoNative(
imageUrl: imageUrl,
videoUrl: videoUrl,
);

View File

@@ -0,0 +1,6 @@
import 'item_piped_video.dart';
ItemPipedVideo getItemPipedVideo(String? imageUrl, String videoUrl) =>
throw UnsupportedError(
'Can not ItemPipedVideo without the packages dart:html or dart:io',
);

View File

@@ -0,0 +1,90 @@
import 'dart:html'; // ignore: avoid_web_libraries_in_flutter
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:feeddeck/utils/constants.dart';
import 'item_piped_video.dart';
/// [_convertVideoUrl] converts the video url to a format that can be used to
/// embed the video in an iframe.
String _convertVideoUrl(String videoUrl) {
if (videoUrl.startsWith('https://piped.video/watch?v=')) {
return videoUrl.replaceFirst(
'https://piped.video/watch?v=',
'https://piped.video/embed/',
);
}
if (videoUrl.startsWith('https://piped.video/')) {
return videoUrl.replaceFirst(
'https://piped.video/',
'https://piped.video/embed/',
);
}
return videoUrl;
}
class ItemPipedVideoWeb extends StatefulWidget implements ItemPipedVideo {
const ItemPipedVideoWeb({
super.key,
required this.imageUrl,
required this.videoUrl,
});
final String? imageUrl;
final String videoUrl;
@override
State<ItemPipedVideoWeb> createState() => _ItemPipedVideoWebState();
}
class _ItemPipedVideoWebState extends State<ItemPipedVideoWeb> {
final IFrameElement _iframeElement = IFrameElement();
@override
void initState() {
super.initState();
_iframeElement.src = _convertVideoUrl(widget.videoUrl);
_iframeElement.style.border = 'none';
_iframeElement.allowFullscreen = true;
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
widget.videoUrl,
(int viewId) => _iframeElement,
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(
bottom: Constants.spacingMiddle,
),
child: LayoutBuilder(
builder: (context, constraints) {
return Center(
child: SizedBox(
width: constraints.maxWidth,
height: constraints.maxWidth * 9.0 / 16.0,
child: HtmlElementView(
key: Key(widget.videoUrl),
viewType: widget.videoUrl,
),
),
);
},
),
);
}
}
ItemPipedVideo getItemPipedVideo(String? imageUrl, String videoUrl) =>
ItemPipedVideoWeb(
imageUrl: imageUrl,
videoUrl: videoUrl,
);

View File

@@ -20,26 +20,94 @@ class ItemSubtitle extends StatelessWidget {
final FDItem item;
final FDSource source;
/// [_buildChildren] is used to build the children of the [RichText] widget.
/// Depending on the provided information we will display the source title,
/// the author and the publishing time of the item. We also include an icon
/// for each information.
List<InlineSpan> _buildChildren() {
final List<InlineSpan> children = [];
if (source.title.trim().isNotEmpty) {
children.addAll([
WidgetSpan(
child: Icon(
source.type.icon,
size: 12,
color: Constants.secondaryTextColor,
),
),
TextSpan(
text: ' ${source.title.trim()}',
),
]);
} else {
children.addAll([
WidgetSpan(
child: Icon(
source.type.icon,
size: 12,
color: Constants.secondaryTextColor,
),
),
TextSpan(
text: ' ${source.type.toLocalizedString()}',
),
]);
}
if (item.author != null && item.author!.trim().isNotEmpty) {
children.addAll([
const TextSpan(
text: ' | ',
),
const WidgetSpan(
child: Icon(
Icons.person,
size: 12,
color: Constants.secondaryTextColor,
),
),
TextSpan(
text: ' ${item.author!.trim()}',
),
]);
}
children.addAll([
const TextSpan(
text: ' | ',
),
const WidgetSpan(
child: Icon(
Icons.schedule,
size: 12,
color: Constants.secondaryTextColor,
),
),
TextSpan(
text:
' ${DateFormat.yMMMMd().add_Hm().format(DateTime.fromMillisecondsSinceEpoch(item.publishedAt * 1000))}',
),
]);
return children;
}
@override
Widget build(BuildContext context) {
final sourceType = source.type.toLocalizedString();
final sourceTitle = source.title != '' ? ' / ${source.title}' : '';
final author =
item.author != null && item.author != '' ? ' / ${item.author}' : '';
final publishedAt =
' / ${DateFormat.yMMMMd().add_Hm().format(DateTime.fromMillisecondsSinceEpoch(item.publishedAt * 1000))}';
return Padding(
padding: const EdgeInsets.only(
bottom: Constants.spacingMiddle,
),
child: SelectableText(
'$sourceType$sourceTitle$author$publishedAt',
child: RichText(
textAlign: TextAlign.left,
style: const TextStyle(
color: Constants.secondaryTextColor,
fontWeight: FontWeight.bold,
fontSize: 10,
text: TextSpan(
style: const TextStyle(
color: Constants.secondaryTextColor,
fontWeight: FontWeight.bold,
fontSize: 10,
),
children: _buildChildren(),
),
),
);

View File

@@ -12,6 +12,10 @@ class ItemTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (itemTitle.isEmpty) {
return Container();
}
return SelectableText(
itemTitle,
textAlign: TextAlign.left,

View File

@@ -0,0 +1,290 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:feeddeck/utils/constants.dart';
/// The [ItemVideos] widget is used to display a list of videos, which can be
/// played by the user. If the [videos] list is empty or null, the widget will
/// not be displayed.
class ItemVideos extends StatelessWidget {
const ItemVideos({
super.key,
required this.videos,
});
final List<String>? videos;
@override
Widget build(BuildContext context) {
if (videos == null || videos!.isEmpty) {
return Container();
}
return ListView.builder(
shrinkWrap: true,
itemCount: videos!.length,
itemBuilder: (context, index) {
return ItemVideoPlayer(video: videos![index]);
},
);
}
}
/// The [ItemVideoQuality] class is used to store the different qualities of a
/// video. It is used in combination with the [ItemVideoPlayer] widget.
class ItemVideoQuality {
const ItemVideoQuality({
required this.quality,
required this.video,
});
final String quality;
final String video;
}
/// The [ItemVideoPlayer] widget is used to display a video, which can be played
/// by the user. It should be used in combination with the [ItemVideos] widget
/// and is responsible for the actual implementation of the video player.
///
/// If the provided [video] doesn't contain the audio stream it can be passed
/// via the [audio] parameter.
///
/// The optional [qualities] parameter can be used to display a list of
/// different qualities for the video, so that a user can select a lower quality
/// if the video is not loading fast enough.
class ItemVideoPlayer extends StatefulWidget {
const ItemVideoPlayer({
super.key,
required this.video,
this.audio,
this.qualities,
});
final String video;
final String? audio;
final List<ItemVideoQuality>? qualities;
@override
State<ItemVideoPlayer> createState() => _ItemVideoPlayerState();
}
class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
late final player = Player();
late final controller = VideoController(player);
/// [_buildQualityButton] returns a button which can be used to display a list
/// of different qualities for the video, so that a user can select a lower
/// quality if the video is not loading fast enough.
Widget _buildQualityButton() {
return IconButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
useSafeArea: true,
elevation: 0,
backgroundColor: Colors.transparent,
constraints: const BoxConstraints(
maxWidth: Constants.centeredFormMaxWidth,
),
builder: (BuildContext context) {
return Container(
margin: const EdgeInsets.all(
Constants.spacingMiddle,
),
padding: const EdgeInsets.only(
left: Constants.spacingMiddle,
right: Constants.spacingMiddle,
),
decoration: const BoxDecoration(
color: Constants.background,
borderRadius: BorderRadius.all(
Radius.circular(Constants.spacingMiddle),
),
),
child: Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: widget.qualities!
.asMap()
.entries
.map((quality) {
if (quality.key == widget.qualities!.length - 1) {
return [
ListTile(
mouseCursor: SystemMouseCursors.click,
onTap: () async {
Navigator.of(context).pop();
await _playerOpen(quality.value.video);
},
title: Text(quality.value.quality),
),
];
}
return [
ListTile(
mouseCursor: SystemMouseCursors.click,
onTap: () async {
Navigator.of(context).pop();
await _playerOpen(quality.value.video);
},
title: Text(quality.value.quality),
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
];
})
.expand((e) => e)
.toList(),
),
);
},
);
},
icon: const Icon(Icons.tune),
);
}
/// [_buildBottomButtonBar] returns the list of buttons which are displayed in
/// the bottom button bar of the video player. If the [qualities] parameter is
/// not null, a button to select the quality of the video is added to the
/// bottom button bar. If the [isMobile] parameter is true, the bottom button
/// bar contains the default buttons from the [MaterialVideoControlsThemeData]
/// theme, if it is false it contains the default buttons from the
/// [MaterialDesktopVideoControlsThemeData] theme.
List<Widget> _buildBottomButtonBar(bool isMobile) {
if (isMobile) {
if (widget.qualities != null) {
return [
const MaterialPositionIndicator(),
const Spacer(),
_buildQualityButton(),
const MaterialFullscreenButton(),
];
}
return const [
MaterialPositionIndicator(),
Spacer(),
MaterialFullscreenButton(),
];
}
if (widget.qualities != null) {
return [
const MaterialDesktopSkipPreviousButton(),
const MaterialDesktopPlayOrPauseButton(),
const MaterialDesktopSkipNextButton(),
const MaterialDesktopVolumeButton(),
const MaterialDesktopPositionIndicator(),
const Spacer(),
_buildQualityButton(),
const MaterialDesktopFullscreenButton(),
];
}
return const [
MaterialDesktopSkipPreviousButton(),
MaterialDesktopPlayOrPauseButton(),
MaterialDesktopSkipNextButton(),
MaterialDesktopVolumeButton(),
MaterialDesktopPositionIndicator(),
Spacer(),
MaterialDesktopFullscreenButton(),
];
}
/// [_playerOpen] opens the video player with the provided [video] and sets
/// the audio track if it is provided via the [audio] parameter.
Future<void> _playerOpen(String video) async {
await player.open(
Media(video),
play: false,
);
/// Load an external audio track when it is provided via the [audio]
/// parameter.
/// See: https://github.com/media-kit/media-kit?tab=readme-ov-file#load-external-audio-track
if (widget.audio != null) {
await player.setAudioTrack(
AudioTrack.uri(
widget.audio!,
),
);
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_playerOpen(widget.video);
});
}
@override
void dispose() {
player.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(
bottom: Constants.spacingMiddle,
),
child: LayoutBuilder(
builder: (context, constraints) {
return Center(
child: SizedBox(
width: constraints.maxWidth,
height: constraints.maxWidth * 9.0 / 16.0,
child: MaterialDesktopVideoControlsTheme(
normal: MaterialDesktopVideoControlsThemeData(
bottomButtonBar: _buildBottomButtonBar(false),
seekBarPositionColor: Constants.primary,
seekBarThumbColor: Constants.primary,
),
fullscreen: const MaterialDesktopVideoControlsThemeData(
seekBarPositionColor: Constants.primary,
seekBarThumbColor: Constants.primary,
),
child: MaterialVideoControlsTheme(
normal: MaterialVideoControlsThemeData(
bottomButtonBar: _buildBottomButtonBar(true),
seekBarPositionColor: Constants.primary,
seekBarThumbColor: Constants.primary,
),
fullscreen: const MaterialVideoControlsThemeData(
seekBarPositionColor: Constants.primary,
seekBarThumbColor: Constants.primary,
),
child: Video(
controller: controller,
controls: kIsWeb ||
Platform.isLinux ||
Platform.isMacOS ||
Platform.isWindows
? MaterialDesktopVideoControls
: MaterialVideoControls,
),
),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,27 @@
library item_youtube_video;
import 'package:flutter/material.dart';
import 'item_youtube_video_stub.dart'
if (dart.library.io) 'item_youtube_video_native.dart'
if (dart.library.html) 'item_youtube_video_web.dart';
/// The [ItemYoutubeVideo] class implements a widget that displays a video from
/// YouTube.
///
/// This is required because we are using different implementations for the web
/// and for all other target platforms (Android, iOS, macOS, Windows, Linux). On
/// the web we display the YouTube video via an `iframe` element. On all other
/// platforms we are using the [youtube_explode_dart] package to fetch the url
/// of the YouTube video, which can then be displayed via our [ItemVideoPlayer]
/// widget.
///
/// We decided for this implementation, because on the web we would have to
/// proxy the calls from the [youtube_explode_dart] because of CORS errors.
/// Further with the former implementation via the [youtube_player_iframe]
/// package we were not able to display the video on macOS, Windows and Linux
/// and we were not able to display the video in fullscreen.
abstract class ItemYoutubeVideo implements StatefulWidget {
factory ItemYoutubeVideo(String? imageUrl, String videoUrl) =>
getItemYoutubeVideo(imageUrl, videoUrl);
}

View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
import 'item_youtube_video.dart';
class ItemYoutubeVideoNative extends StatefulWidget
implements ItemYoutubeVideo {
const ItemYoutubeVideoNative({
super.key,
required this.imageUrl,
required this.videoUrl,
});
final String? imageUrl;
final String videoUrl;
@override
State<ItemYoutubeVideoNative> createState() => _ItemYoutubeVideoNativeState();
}
class _ItemYoutubeVideoNativeState extends State<ItemYoutubeVideoNative> {
final yt = YoutubeExplode();
late Future<List<ItemVideoQuality>> _futureFetchVideoUrls;
Future<List<ItemVideoQuality>> _fetchVideoUrls() async {
final streamManifest = await yt.videos.streamsClient.getManifest(
widget.videoUrl,
);
return streamManifest.muxed
.sortByVideoQuality()
.map(
(element) => ItemVideoQuality(
quality: element.qualityLabel,
video: element.url.toString(),
),
)
.toList();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
setState(() {
_futureFetchVideoUrls = _fetchVideoUrls();
});
}
@override
void dispose() {
yt.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _futureFetchVideoUrls,
builder: (
BuildContext context,
AsyncSnapshot<List<ItemVideoQuality>> snapshot,
) {
if (snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasError ||
snapshot.data == null ||
snapshot.data!.isEmpty) {
return ItemMedia(itemMedia: widget.imageUrl);
}
return ItemVideoPlayer(
video: snapshot.data!.first.video,
qualities: snapshot.data,
);
},
);
}
}
ItemYoutubeVideo getItemYoutubeVideo(String? imageUrl, String videoUrl) =>
ItemYoutubeVideoNative(
imageUrl: imageUrl,
videoUrl: videoUrl,
);

View File

@@ -0,0 +1,6 @@
import 'item_youtube_video.dart';
ItemYoutubeVideo getItemYoutubeVideo(String? imageUrl, String videoUrl) =>
throw UnsupportedError(
'Can not ItemYoutubeVideo without the packages dart:html or dart:io',
);

View File

@@ -0,0 +1,97 @@
import 'dart:html'; // ignore: avoid_web_libraries_in_flutter
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:feeddeck/utils/constants.dart';
import 'item_youtube_video.dart';
/// [_convertVideoUrl] converts the video url to a format that can be used to
/// embed the video in an iframe.
String _convertVideoUrl(String videoUrl) {
if (videoUrl.startsWith('https://youtu.be/')) {
return videoUrl.replaceFirst(
'https://youtu.be/',
'https://www.youtube-nocookie.com/embed/',
);
}
if (videoUrl.startsWith('https://www.youtube.com/watch?v=')) {
return 'https://www.youtube-nocookie.com/embed/${videoUrl.replaceFirst(
'https://www.youtube.com/watch?v=',
'',
)}';
}
if (videoUrl.startsWith('https://m.youtube.com/watch?v=')) {
return 'https://www.youtube-nocookie.com/embed/${videoUrl.replaceFirst(
'https://m.youtube.com/watch?v=',
'',
)}';
}
return videoUrl;
}
class ItemYoutubeVideoWeb extends StatefulWidget implements ItemYoutubeVideo {
const ItemYoutubeVideoWeb({
super.key,
required this.imageUrl,
required this.videoUrl,
});
final String? imageUrl;
final String videoUrl;
@override
State<ItemYoutubeVideoWeb> createState() => _ItemYoutubeVideoWebState();
}
class _ItemYoutubeVideoWebState extends State<ItemYoutubeVideoWeb> {
final IFrameElement _iframeElement = IFrameElement();
@override
void initState() {
super.initState();
_iframeElement.src = _convertVideoUrl(widget.videoUrl);
_iframeElement.style.border = 'none';
_iframeElement.allowFullscreen = true;
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
widget.videoUrl,
(int viewId) => _iframeElement,
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(
bottom: Constants.spacingMiddle,
),
child: LayoutBuilder(
builder: (context, constraints) {
return Center(
child: SizedBox(
width: constraints.maxWidth,
height: constraints.maxWidth * 9.0 / 16.0,
child: HtmlElementView(
key: Key(widget.videoUrl),
viewType: widget.videoUrl,
),
),
);
},
),
);
}
}
ItemYoutubeVideo getItemYoutubeVideo(String? imageUrl, String videoUrl) =>
ItemYoutubeVideoWeb(
imageUrl: imageUrl,
videoUrl: videoUrl,
);

View File

@@ -7,9 +7,11 @@ import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/items_repository.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_github.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_googlenews.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_lemmy.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_mastodon.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_medium.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_nitter.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_pinterest.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_podcast.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_reddit.dart';
import 'package:feeddeck/widgets/item/preview/item_preview_rss.dart';
@@ -52,6 +54,11 @@ class ItemPreview extends StatelessWidget {
item: item,
source: source,
);
case FDSourceType.lemmy:
return ItemPreviewLemmy(
item: item,
source: source,
);
case FDSourceType.mastodon:
return ItemPreviewMastodon(
item: item,
@@ -67,6 +74,11 @@ class ItemPreview extends StatelessWidget {
item: item,
source: source,
);
case FDSourceType.pinterest:
return ItemPreviewPinterest(
item: item,
source: source,
);
case FDSourceType.podcast:
return ItemPreviewPodcast(
item: item,

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/utils/image_url.dart';
import 'package:feeddeck/widgets/item/preview/utils/details.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_actions.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_description.dart';
@@ -30,7 +29,6 @@ class ItemPreviewGooglenews extends StatelessWidget {
sourceSubtitle: '${source.type.toLocalizedString()}: ${source.title}',
sourceType: source.type,
sourceIcon: item.media,
sourceIconType: FDImageType.item,
itemPublishedAt: item.publishedAt,
itemIsRead: item.isRead,
),

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/widgets/item/preview/utils/details.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_actions.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_description.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_media.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_source.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_title.dart';
class ItemPreviewLemmy extends StatelessWidget {
const ItemPreviewLemmy({
super.key,
required this.item,
required this.source,
});
final FDItem item;
final FDSource source;
/// [_buildMedia] returns the media of the item if the item has media file.
/// Since we save images and videos within the media property we have to
/// filter out all videos.
///
/// See the `getMedia` function in the `lemmy.ts` file, for a list of
/// extension which are a image / video.
Widget _buildMedia() {
if (item.media != null && item.media! != '') {
final mediaUrl = Uri.parse(item.media!);
if (mediaUrl.path.endsWith('.jpg') ||
mediaUrl.path.endsWith('.jpeg') ||
mediaUrl.path.endsWith('.png') ||
mediaUrl.path.endsWith('.gif')) {
return ItemMedia(
itemMedia: item.media,
);
}
}
return Container();
}
@override
Widget build(BuildContext context) {
return ItemActions(
item: item,
onTap: () => showDetails(context, item, source),
children: [
ItemSource(
sourceTitle: source.title,
sourceSubtitle: source.type.toLocalizedString(),
sourceType: source.type,
sourceIcon: source.icon,
itemPublishedAt: item.publishedAt,
itemIsRead: item.isRead,
),
ItemTitle(
itemTitle: item.title,
),
_buildMedia(),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.plain,
),
],
);
}
}

View File

@@ -38,7 +38,9 @@ class ItemPreviewMastodon extends StatelessWidget {
tagetFormat: DescriptionFormat.markdown,
),
ItemMediaGallery(
itemMedias: item.options != null && item.options!.containsKey('media')
itemMedias: item.options != null &&
item.options!.containsKey('media') &&
item.options!['media'] != null
? (item.options!['media'] as List)
.map((item) => item as String)
.toList()

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/widgets/item/preview/utils/details.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_actions.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_description.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_media.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_source.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_title.dart';
class ItemPreviewPinterest extends StatelessWidget {
const ItemPreviewPinterest({
super.key,
required this.item,
required this.source,
});
final FDItem item;
final FDSource source;
@override
Widget build(BuildContext context) {
return ItemActions(
item: item,
onTap: () => showDetails(context, item, source),
children: [
ItemSource(
sourceTitle: source.title,
sourceSubtitle: source.type.toLocalizedString(),
sourceType: source.type,
sourceIcon: source.icon,
itemPublishedAt: item.publishedAt,
itemIsRead: item.isRead,
),
ItemTitle(
itemTitle: item.title,
),
ItemMedia(
itemMedia: item.media,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.plain,
),
],
);
}
}

View File

@@ -41,7 +41,7 @@ class ItemPreviewRSS extends StatelessWidget {
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.plain,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.plain,
),
],

View File

@@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/repositories/items_repository.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/openurl.dart';
/// The [ItemActions] widget provides an actions menu for an item, which can be
/// used to quickly mark an item as read or unread and to add or remove a
@@ -62,6 +63,14 @@ class _ItemActionsState extends State<ItemActions> {
} catch (_) {}
}
/// [_openUrl] opens the item url in the default browser of the current
/// device.
Future<void> _openUrl() async {
try {
await openUrl(widget.item.link);
} catch (_) {}
}
/// [_getTapPositionLarge] set the [_tapPosition] which will be used for the
/// actions menu.
void _getTapPositionLarge(TapDownDetails details) {
@@ -70,7 +79,7 @@ class _ItemActionsState extends State<ItemActions> {
});
}
/// [_showActionsMenuLarge] shows a popup menu with all available aactions for
/// [_showActionsMenuLarge] shows a popup menu with all available actions for
/// an item. This means the user can mark an item as read or unread or a user
/// can add or remove a bookmark for an item.
void _showActionsMenuLarge(BuildContext context) async {
@@ -126,6 +135,15 @@ class _ItemActionsState extends State<ItemActions> {
: const Text('Add Bookmark'),
),
),
const PopupMenuItem(
value: 'openlink',
child: ListTile(
leading: Icon(
Icons.launch,
),
title: Text('Open Link'),
),
),
],
);
@@ -140,9 +158,19 @@ class _ItemActionsState extends State<ItemActions> {
await _bookmark(context);
}
break;
case 'openlink':
if (mounted) {
await _openUrl();
}
break;
}
}
/// [_showActionsMenuSmall] shows a modal bottom sheet with all available
/// actions for an item. This means the user can mark an item as read or
/// unread or a user can add or remove a bookmark for an item. The actions are
/// the same as we show on large screens via [_showActionsMenuLarge], but the
/// modal bottom sheet is optiomized for small screens.
void _showActionsMenuSmall(BuildContext mainContext) async {
HapticFeedback.heavyImpact();
@@ -206,6 +234,20 @@ class _ItemActionsState extends State<ItemActions> {
? const Text('Remove Bookmark')
: const Text('Add Bookmark'),
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
ListTile(
mouseCursor: SystemMouseCursors.click,
onTap: () async {
Navigator.of(context).pop();
_openUrl();
},
leading: const Icon(Icons.launch),
title: const Text('Open Link'),
),
],
),
);
@@ -326,12 +368,17 @@ class _ItemActionsState extends State<ItemActions> {
/// On large screens we show an actions menu via `_showActionsMenuLarge`,
/// which is rendered directly at the point where the user pressed on the
/// item.
/// The menu can be opened by a long press or by a secondary tap (right
/// click) on the item.
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: widget.onTap,
onTapDown: (details) => _getTapPositionLarge(details),
onLongPress: () => _showActionsMenuLarge(context),
onSecondaryTapDown:
kIsWeb ? null : (details) => _getTapPositionLarge(details),
onSecondaryTap: kIsWeb ? null : () => _showActionsMenuLarge(context),
child: Container(
width: double.infinity,
decoration: const BoxDecoration(

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:html2md/html2md.dart' as html2md;
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/font.dart';
import 'package:feeddeck/utils/openurl.dart';
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
/// The [DescriptionFormat] enum defines the source and target format of a
/// description.
@@ -46,7 +46,7 @@ class ItemDescription extends StatelessWidget {
),
child: MarkdownBody(
selectable: false,
data: content,
data: content.trim(),
styleSheet: MarkdownStyleSheet(
code: TextStyle(
fontFamily: getMonospaceFontFamily(),
@@ -81,13 +81,21 @@ class ItemDescription extends StatelessWidget {
}
/// [_buildPlain] renders the provided [content] as plain text.
///
/// To not have some trailing newlines, the [content] is trimmed and splitted
/// on newline characters, so that we can filter out empty lines, before the
/// the content is rendered.
Widget _buildPlain(String content) {
if (content == '') {
return Container();
}
return Container(
padding: const EdgeInsets.only(
bottom: Constants.spacingExtraSmall,
),
child: Text(
content,
content.trim().split('\n').where((line) => line != '').join('\n'),
maxLines: 5,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
@@ -117,7 +125,9 @@ class ItemDescription extends StatelessWidget {
if (sourceFormat == DescriptionFormat.html &&
tagetFormat == DescriptionFormat.plain) {
return _buildPlain(
itemDescription!.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ''),
itemDescription!
.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ')
.replaceAll(RegExp('\\s+'), ' '),
);
}

View File

@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/image_url.dart';
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
/// The [ItemMedia] widget displays the media of an item. Based on the provided
/// [itemMedia] value the media is displayed from the Supabase storage or
@@ -30,7 +28,7 @@ class ItemMedia extends StatelessWidget {
width: double.infinity,
height: 200,
fit: BoxFit.cover,
imageUrl: getImageUrl(FDImageType.item, itemMedia!),
imageUrl: itemMedia!,
placeholder: (context, url) => Container(),
errorWidget: (context, url, error) => Container(),
),

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/image_url.dart';
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
/// The [ItemMediaGallery] widget can be used to display multiple media files in
/// a gallery. Similar to the [ItemMedia] widget the provided [itemMedias]
@@ -24,12 +22,10 @@ class ItemMediaGallery extends StatelessWidget {
/// [CachedNetworkImage] widget. If the app is running in the web, the url is
/// proxied through the Supabase functions.
Widget _buildSingleMedia(BuildContext context, String itemMedia) {
final imageUrl = getImageUrl(FDImageType.item, itemMedia);
return CachedNetworkImage(
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
imageUrl: imageUrl,
imageUrl: itemMedia,
placeholder: (context, url) => Container(),
errorWidget: (context, url, error) => Container(),
);

View File

@@ -4,7 +4,6 @@ import 'package:timeago/timeago.dart' as timeago;
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/image_url.dart';
import 'package:feeddeck/widgets/source/source_icon.dart';
/// The [ItemSource] widget is used to display the source of an item above the
@@ -16,7 +15,6 @@ class ItemSource extends StatelessWidget {
required this.sourceSubtitle,
required this.sourceType,
required this.sourceIcon,
this.sourceIconType = FDImageType.source,
required this.itemPublishedAt,
required this.itemIsRead,
});
@@ -25,7 +23,6 @@ class ItemSource extends StatelessWidget {
final String sourceSubtitle;
final FDSourceType sourceType;
final String? sourceIcon;
final FDImageType sourceIconType;
final int itemPublishedAt;
final bool itemIsRead;
@@ -72,7 +69,6 @@ class ItemSource extends StatelessWidget {
child: SourceIcon(
type: sourceType,
icon: sourceIcon,
iconType: sourceIconType,
size: 32,
),
),

View File

@@ -27,14 +27,22 @@ class _ResetPasswordState extends State<ResetPassword> {
/// [_validatePassword] validates the email address provided via the
/// [TextField] of the [_passwordController]. The password field can not be
/// empty and must have a minimum length of 6 characters.
/// empty and must have a minimum length of 8 characters. The password must
/// also contain at least one upper case letter, one lower case letter and one
/// number.
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be a least 6 characters long';
if (value.length < 8) {
return 'Password must be a least 8 characters long';
}
String pattern = r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$';
RegExp regExp = RegExp(pattern);
if (!regExp.hasMatch(value)) {
return 'Password must contain at least one upper case letter, one lower case letter and one number';
}
return null;

View File

@@ -232,78 +232,80 @@ class _SettingsAccountsGithubAddState extends State<SettingsAccountsGithubAdd> {
),
],
),
body: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
selectable: true,
data: _helpText,
onTapLink: (text, href, title) {
try {
if (href != null) {
openUrl(href);
}
} catch (_) {}
},
),
const SizedBox(
height: Constants.spacingMiddle,
),
TextFormField(
controller: _tokenController,
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: true,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Token',
body: SafeArea(
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
selectable: true,
data: _helpText,
onTapLink: (text, href, title) {
try {
if (href != null) {
openUrl(href);
}
} catch (_) {}
},
),
validator: (value) => _validateToken(value),
onFieldSubmitted: (value) => _addAccount(),
),
],
const SizedBox(
height: Constants.spacingMiddle,
),
TextFormField(
controller: _tokenController,
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: true,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Token',
),
validator: (value) => _validateToken(value),
onFieldSubmitted: (value) => _addAccount(),
),
],
),
),
),
),
),
),
const SizedBox(
height: Constants.spacingSmall,
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
_buildError(),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
label: const Text('Add Account'),
onPressed: _isLoading ? null : _addAccount,
icon: _isLoading
? const ElevatedButtonProgressIndicator()
: const Icon(Icons.add),
const SizedBox(
height: Constants.spacingSmall,
),
),
],
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
_buildError(),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
label: const Text('Add Account'),
onPressed: _isLoading ? null : _addAccount,
icon: _isLoading
? const ElevatedButtonProgressIndicator()
: const Icon(Icons.add),
),
),
],
),
),
);
}

View File

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/repositories/items_repository.dart';
import 'package:feeddeck/repositories/layout_repository.dart';
import 'package:feeddeck/utils/constants.dart';
/// The [SettingsDecksSelect] widget shows a list of the users decks, when the
@@ -20,13 +21,18 @@ class _SettingsDecksSelectState extends State<SettingsDecksSelect> {
/// [_selectDeck] sets the provided [deckId] as the active deck. The active
/// deck is updated via the [selectDeck] method of the [AppRepository]. When
/// the active deck is updated the user is redirected to the decks view.
///
/// Before the active deck is changed the [ItemsRepositoryStore] is cleared,
/// to trigger a reload of the items once the deck is loaded.
Future<void> _selectDeck(String deckId) async {
try {
/// Before the active deck is changed the [ItemsRepositoryStore] is
/// cleared, to trigger a reload of the items once the deck is loaded.
ItemsRepositoryStore().clear();
/// We also have to reset the tab index when the user selects a new deck,
/// so that the first tab is selected instead of the tab with the same
/// index as in the previously selected deck.
Provider.of<LayoutRepository>(context, listen: false)
.deckLayoutSmallInitialTabIndex = 0;
await Provider.of<AppRepository>(context, listen: false)
.selectDeck(deckId);
if (!mounted) return;

View File

@@ -166,28 +166,29 @@ class _SettingsPremiumInAppState extends State<SettingsPremiumInApp> {
),
],
),
body: FutureBuilder(
future: _futureFetchOfferings,
builder: (
BuildContext context,
AsyncSnapshot<Offering?> snapshot,
) {
return Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.hasError ||
snapshot.data == null ||
snapshot.data?.monthly == null
? const Text('Loading ...')
: MarkdownBody(
selectable: true,
data: '''
body: SafeArea(
child: FutureBuilder(
future: _futureFetchOfferings,
builder: (
BuildContext context,
AsyncSnapshot<Offering?> snapshot,
) {
return Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.hasError ||
snapshot.data == null ||
snapshot.data?.monthly == null
? const Text('Loading ...')
: MarkdownBody(
selectable: true,
data: '''
You are currently using the free version of FeedDeck, which allows you to add up
to 10 sources for the first 7 days. After that trial period your sources will
not be updated anymore.
@@ -197,57 +198,61 @@ upgrade to a premium account. The premium account costs
${snapshot.data?.monthly?.storeProduct.priceString} per month and can be
canceled at any time.
''',
),
),
),
),
const SizedBox(
height: Constants.spacingSmall,
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Constants.primary,
foregroundColor: Constants.onPrimary,
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
),
label: Text(
snapshot.data?.monthly?.storeProduct.priceString != null
? 'Subscribe to FeedDeck Premium for ${snapshot.data?.monthly?.storeProduct.priceString}'
: 'Subscribe to FeedDeck Premium',
),
onPressed: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasError ||
snapshot.data == null ||
snapshot.data?.monthly == null ||
_isLoading
? null
: () => _purchase(snapshot.data!.monthly!),
icon: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasError ||
snapshot.data == null ||
snapshot.data?.monthly == null ||
_isLoading
? const ElevatedButtonProgressIndicator()
: const Icon(FDIcons.feeddeck),
),
),
],
);
},
const SizedBox(
height: Constants.spacingSmall,
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Constants.primary,
foregroundColor: Constants.onPrimary,
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
label: Text(
snapshot.data?.monthly?.storeProduct.priceString != null
? 'Subscribe to FeedDeck Premium for ${snapshot.data?.monthly?.storeProduct.priceString}'
: 'Subscribe to FeedDeck Premium',
),
onPressed:
snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.hasError ||
snapshot.data == null ||
snapshot.data?.monthly == null ||
_isLoading
? null
: () => _purchase(snapshot.data!.monthly!),
icon: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.hasError ||
snapshot.data == null ||
snapshot.data?.monthly == null ||
_isLoading
? const ElevatedButtonProgressIndicator()
: const Icon(FDIcons.feeddeck),
),
),
],
);
},
),
),
);
}

View File

@@ -87,62 +87,67 @@ class _SettingsPremiumStripeState extends State<SettingsPremiumStripe> {
),
],
),
body: FutureBuilder(
future: _futureFetchCheckoutSessionLink,
builder: (
BuildContext context,
AsyncSnapshot<String> snapshot,
) {
return Column(
children: [
const Expanded(
child: Padding(
padding: EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: MarkdownBody(
selectable: true,
data: _settingsPremiumStripeText,
body: SafeArea(
child: FutureBuilder(
future: _futureFetchCheckoutSessionLink,
builder: (
BuildContext context,
AsyncSnapshot<String> snapshot,
) {
return Column(
children: [
const Expanded(
child: Padding(
padding: EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: MarkdownBody(
selectable: true,
data: _settingsPremiumStripeText,
),
),
),
),
),
const SizedBox(
height: Constants.spacingSmall,
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Constants.primary,
foregroundColor: Constants.onPrimary,
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
label: const Text('Subscribe to FeedDeck Premium'),
onPressed: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasError
? null
: () => _openUrl(snapshot.data),
icon: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasError
? const ElevatedButtonProgressIndicator()
: const Icon(FDIcons.feeddeck),
const SizedBox(
height: Constants.spacingSmall,
),
),
],
);
},
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Constants.primary,
foregroundColor: Constants.onPrimary,
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
label: const Text('Subscribe to FeedDeck Premium'),
onPressed:
snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.hasError
? null
: () => _openUrl(snapshot.data),
icon: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.hasError
? const ElevatedButtonProgressIndicator()
: const Icon(FDIcons.feeddeck),
),
),
],
);
},
),
),
);
}

View File

@@ -180,60 +180,65 @@ class _SettingsProfileCustomerPortalModalState
),
],
),
body: FutureBuilder(
future: _futureFetchCustomerPortalLink,
builder: (
BuildContext context,
AsyncSnapshot<String> snapshot,
) {
return Column(
children: [
const Expanded(
child: Padding(
padding: EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: MarkdownBody(
selectable: true,
data: _settingsProfileCustomerPortalText,
body: SafeArea(
child: FutureBuilder(
future: _futureFetchCustomerPortalLink,
builder: (
BuildContext context,
AsyncSnapshot<String> snapshot,
) {
return Column(
children: [
const Expanded(
child: Padding(
padding: EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: MarkdownBody(
selectable: true,
data: _settingsProfileCustomerPortalText,
),
),
),
),
),
const SizedBox(
height: Constants.spacingSmall,
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
label: const Text('Open Customer Portal'),
onPressed: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasError
? null
: () => _openUrl(snapshot.data),
icon: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasError
? const ElevatedButtonProgressIndicator()
: const Icon(Icons.receipt),
const SizedBox(
height: Constants.spacingSmall,
),
),
],
);
},
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
label: const Text('Open Customer Portal'),
onPressed:
snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.hasError
? null
: () => _openUrl(snapshot.data),
icon: snapshot.connectionState == ConnectionState.none ||
snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.hasError
? const ElevatedButtonProgressIndicator()
: const Icon(Icons.receipt),
),
),
],
);
},
),
),
);
}

View File

@@ -24,14 +24,22 @@ class _SettingsProfilePasswordState extends State<SettingsProfilePassword> {
/// [_validatePassword] validates the email address provided via the
/// [TextField] of the [_newPasswordController]. The password field can not be
/// empty and must have a minimum length of 6 characters.
/// empty and must have a minimum length of 8 characters. The password must
/// also contain at least one upper case letter, one lower case letter and one
/// number.
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be a least 6 characters long';
if (value.length < 8) {
return 'Password must be a least 8 characters long';
}
String pattern = r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$';
RegExp regExp = RegExp(pattern);
if (!regExp.hasMatch(value)) {
return 'Password must contain at least one upper case letter, one lower case letter and one number';
}
return null;

View File

@@ -20,18 +20,21 @@ class _SettingsProfileSignOutState extends State<SettingsProfileSignOut> {
bool _isLoading = false;
/// [_signOut] signs out the currently authenticated user and redirects him
/// to the [SignIn] screen. This will sign out the user from all devices.
/// to the [SignIn] screen. If the provided scope is
/// [supabase.SignOutScope.local] the user will be signed out from the current
/// device. If the scope in [supabase.SignOutScope.global] the user will be
/// signed out from all devices.
///
/// Before the user is signed out the [ItemsRepositoryStore] is cleared, to
/// trigger a reload of the items once the user is signed in again.
Future<void> _signOut() async {
Future<void> _signOut(supabase.SignOutScope scope) async {
setState(() {
_isLoading = true;
});
try {
ItemsRepositoryStore().clear();
await supabase.Supabase.instance.client.auth.signOut();
await supabase.Supabase.instance.client.auth.signOut(scope: scope);
setState(() {
_isLoading = false;
@@ -64,7 +67,27 @@ class _SettingsProfileSignOutState extends State<SettingsProfileSignOut> {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => _signOut(),
onTap: () {
/// Show a modal bottom sheet with the [SettingsProfileSignOutActions]
/// widget, where the user can select the scope of the sign out
/// action.
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
useSafeArea: true,
elevation: 0,
backgroundColor: Colors.transparent,
constraints: const BoxConstraints(
maxWidth: Constants.centeredFormMaxWidth,
),
builder: (BuildContext context) {
return SettingsProfileSignOutActions(
signOut: _signOut,
);
},
);
},
child: Card(
color: Constants.secondary,
margin: const EdgeInsets.only(
@@ -115,3 +138,75 @@ class _SettingsProfileSignOutState extends State<SettingsProfileSignOut> {
);
}
}
/// The [SettingsProfileSignOutActions] widget displays a list of actions which
/// can be used to sign out the user from the current device or from all
/// devices.
class SettingsProfileSignOutActions extends StatelessWidget {
const SettingsProfileSignOutActions({
super.key,
required this.signOut,
});
final Future<void> Function(supabase.SignOutScope scope) signOut;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(
Constants.spacingMiddle,
),
padding: const EdgeInsets.only(
left: Constants.spacingMiddle,
right: Constants.spacingMiddle,
),
decoration: const BoxDecoration(
color: Constants.background,
borderRadius: BorderRadius.all(
Radius.circular(Constants.spacingMiddle),
),
),
child: Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
ListTile(
mouseCursor: SystemMouseCursors.click,
onTap: () {
Navigator.of(context).pop();
signOut(supabase.SignOutScope.local);
},
leading: const Icon(
Icons.logout,
),
title: const Text(
'From current device',
),
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
ListTile(
mouseCursor: SystemMouseCursors.click,
onTap: () {
Navigator.of(context).pop();
signOut(supabase.SignOutScope.global);
},
leading: const Icon(
Icons.logout,
color: Constants.error,
),
title: const Text(
'From all devices',
style: TextStyle(
color: Constants.error,
),
),
),
],
),
);
}
}

View File

@@ -13,7 +13,7 @@ import 'package:feeddeck/utils/openurl.dart';
/// Here the user can find information the version of the app and the links to
/// the website, the GitHub repository and the X account.
class SettingsInfo extends StatefulWidget {
const SettingsInfo({Key? key}) : super(key: key);
const SettingsInfo({super.key});
@override
State<SettingsInfo> createState() => _SettingsInfoState();

View File

@@ -10,8 +10,9 @@ import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/repositories/settings_repository.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/desktop_login_manager.dart';
import 'package:feeddeck/utils/desktop_signin_manager.dart';
import 'package:feeddeck/utils/fd_icons.dart';
import 'package:feeddeck/utils/signin_with_apple.dart';
import 'package:feeddeck/widgets/deck/deck_layout.dart';
import 'package:feeddeck/widgets/general/elevated_button_progress_indicator.dart';
import 'package:feeddeck/widgets/general/logo.dart';
@@ -65,7 +66,7 @@ class _SignInState extends State<SignIn> {
);
await supabase.Supabase.instance.client.auth.signInWithIdToken(
provider: supabase.Provider.google,
provider: supabase.OAuthProvider.google,
idToken: idToken,
);
@@ -90,17 +91,17 @@ class _SignInState extends State<SignIn> {
);
} else if (!kIsWeb &&
(Platform.isLinux || Platform.isMacOS || Platform.isWindows)) {
/// On Linux, macOS and Windows we have to use the [DesktopLoginManager]
/// to handle the login via the users Google account. Once the sing in
/// process is finished we have to call the init method of the
/// [AppRepository] to load the users data.
/// On Linux, macOS and Windows we have to use the
/// [DesktopSignInManager] to handle the login via the users Google
/// account. Once the sing in process is finished we have to call the
/// init method of the [AppRepository] to load the users data.
setState(() {
_isLoading = true;
_error = '';
});
await DesktopLoginManager(
provider: supabase.Provider.google,
await DesktopSignInManager(
provider: supabase.OAuthProvider.google,
queryParams: {
'access_type': 'offline',
'prompt': 'consent',
@@ -134,7 +135,7 @@ class _SignInState extends State<SignIn> {
/// method of the [AppRepository] is automatically called. On iOS
/// the authentication is the handled via the `singin-callback` route.
await supabase.Supabase.instance.client.auth.signInWithOAuth(
supabase.Provider.google,
supabase.OAuthProvider.google,
queryParams: {
'access_type': 'offline',
'prompt': 'consent',
@@ -165,7 +166,7 @@ class _SignInState extends State<SignIn> {
_error = '';
});
await supabase.Supabase.instance.client.auth.signInWithApple();
await signInWithApple();
if (!mounted) return;
await Provider.of<AppRepository>(
@@ -187,7 +188,7 @@ class _SignInState extends State<SignIn> {
(route) => false,
);
} else if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
/// On Linux and Windows we have to use the [DesktopLoginManager] to
/// On Linux and Windows we have to use the [DesktopSignInManager] to
/// handle the login via the users Apple account. Once the sing in
/// process is finished we have to call the init method of the
/// [AppRepository] to load the users data.
@@ -196,8 +197,8 @@ class _SignInState extends State<SignIn> {
_error = '';
});
await DesktopLoginManager(
provider: supabase.Provider.apple,
await DesktopSignInManager(
provider: supabase.OAuthProvider.apple,
queryParams: null,
).signIn();
@@ -228,7 +229,7 @@ class _SignInState extends State<SignIn> {
/// method of the [AppRepository] is automatically called. On Android
/// the authentication is the handled via the `singin-callback` route.
await supabase.Supabase.instance.client.auth.signInWithOAuth(
supabase.Provider.apple,
supabase.OAuthProvider.apple,
redirectTo:
kIsWeb ? null : 'app.feeddeck.feeddeck://signin-callback/',
);

View File

@@ -47,18 +47,15 @@ class _SignInWithFeedDeckState extends State<SignInWithFeedDeck> {
return null;
}
/// [_validatePassword] validates the email address provided via the
/// [TextField] of the [_passwordController]. The password field can not be
/// empty and must have a minimum length of 6 characters.
/// [_validatePassword] validates the password provided via the [TextField] of
/// the [_passwordController]. In opposite to the sign up, reset password and
/// change password validations, we just check that the password field is not
/// empty.
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be a least 6 characters long';
}
return null;
}

View File

@@ -56,14 +56,22 @@ class _SignUpState extends State<SignUp> {
/// [_validatePassword] validates the email address provided via the
/// [TextField] of the [_passwordController]. The password field can not be
/// empty and must have a minimum length of 6 characters.
/// empty and must have a minimum length of 8 characters. The password must
/// also contain at least one upper case letter, one lower case letter and one
/// number.
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be a least 6 characters long';
if (value.length < 8) {
return 'Password must be a least 8 characters long';
}
String pattern = r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$';
RegExp regExp = RegExp(pattern);
if (!regExp.hasMatch(value)) {
return 'Password must contain at least one upper case letter, one lower case letter and one number';
}
return null;

View File

@@ -5,9 +5,11 @@ import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/widgets/source/add/add_source_github.dart';
import 'package:feeddeck/widgets/source/add/add_source_googlenews.dart';
import 'package:feeddeck/widgets/source/add/add_source_lemmy.dart';
import 'package:feeddeck/widgets/source/add/add_source_mastodon.dart';
import 'package:feeddeck/widgets/source/add/add_source_medium.dart';
import 'package:feeddeck/widgets/source/add/add_source_nitter.dart';
import 'package:feeddeck/widgets/source/add/add_source_pinterest.dart';
import 'package:feeddeck/widgets/source/add/add_source_podcast.dart';
import 'package:feeddeck/widgets/source/add/add_source_reddit.dart';
import 'package:feeddeck/widgets/source/add/add_source_rss.dart';
@@ -51,6 +53,10 @@ class _AddSourceState extends State<AddSource> {
return AddSourceGoogleNews(column: widget.column);
}
if (_sourceType == FDSourceType.lemmy) {
return AddSourceLemmy(column: widget.column);
}
if (_sourceType == FDSourceType.mastodon) {
return AddSourceMastodon(column: widget.column);
}
@@ -63,6 +69,10 @@ class _AddSourceState extends State<AddSource> {
return AddSourceNitter(column: widget.column);
}
if (_sourceType == FDSourceType.pinterest) {
return AddSourcePinterst(column: widget.column);
}
if (_sourceType == FDSourceType.podcast) {
return AddSourcePodcast(column: widget.column);
}
@@ -75,6 +85,10 @@ class _AddSourceState extends State<AddSource> {
return AddSourceRSS(column: widget.column);
}
if (_sourceType == FDSourceType.stackoverflow) {
return AddSourceStackOverflow(column: widget.column);
}
if (_sourceType == FDSourceType.tumblr) {
return AddSourceTumblr(column: widget.column);
}
@@ -83,10 +97,6 @@ class _AddSourceState extends State<AddSource> {
// return AddSourceX(column: widget.column);
// }
if (_sourceType == FDSourceType.stackoverflow) {
return AddSourceStackOverflow(column: widget.column);
}
if (_sourceType == FDSourceType.youtube) {
return AddSourceYouTube(column: widget.column);
}
@@ -115,7 +125,7 @@ class _AddSourceState extends State<AddSource> {
/// If we decide later to use a generic color as background
/// the following line can be used:
/// color: Constants.secondary,
color: FDSourceType.values[index].color,
color: FDSourceType.values[index].bgColor,
borderRadius: BorderRadius.circular(4),
),
child: Column(
@@ -132,14 +142,14 @@ class _AddSourceState extends State<AddSource> {
),
Text(
FDSourceType.values[index].toLocalizedString(),
style: const TextStyle(
style: TextStyle(
/// Since we are using the brand color as background
/// color, we are using the same color as for the icon
/// as text color (source_icon.dart). If we decide later
/// to use a generic color as background the following
/// line can be used:
/// color: Constants.onSecondary,
color: Color(0xffffffff),
color: FDSourceType.values[index].fgColor,
),
),
],

View File

@@ -143,6 +143,7 @@ class _AddSourceGitHubState extends State<AddSourceGitHub> {
border: OutlineInputBorder(),
labelText: 'Repository',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
],
@@ -162,6 +163,7 @@ class _AddSourceGitHubState extends State<AddSourceGitHub> {
border: OutlineInputBorder(),
labelText: 'Query Name',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
TextFormField(
@@ -174,6 +176,7 @@ class _AddSourceGitHubState extends State<AddSourceGitHub> {
border: OutlineInputBorder(),
labelText: 'Query',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
],
@@ -193,6 +196,7 @@ class _AddSourceGitHubState extends State<AddSourceGitHub> {
border: OutlineInputBorder(),
labelText: 'User',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
],
@@ -212,6 +216,7 @@ class _AddSourceGitHubState extends State<AddSourceGitHub> {
border: OutlineInputBorder(),
labelText: 'Repository',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
],
@@ -232,6 +237,7 @@ class _AddSourceGitHubState extends State<AddSourceGitHub> {
border: OutlineInputBorder(),
labelText: 'Organization',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
],

View File

@@ -109,6 +109,7 @@ class _AddSourceGoogleNewsState extends State<AddSourceGoogleNews> {
border: OutlineInputBorder(),
labelText: 'Url',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
];
@@ -126,6 +127,7 @@ class _AddSourceGoogleNewsState extends State<AddSourceGoogleNews> {
border: OutlineInputBorder(),
labelText: 'Search',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
DropdownButton<GoogleNewsCode>(

View File

@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:provider/provider.dart';
import 'package:feeddeck/models/column.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/utils/api_exception.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/openurl.dart';
import 'package:feeddeck/widgets/source/add/add_source_form.dart';
const _helpText = '''
The Lemmy source can be used to follow your favorite Lemmy communities.
- **Community**: Provide the url of the community you want to follow
(e.g. `https://lemmy.world/c/lemmyworld`).
- **User**: Provide the url of the user you want to follow
(e.g. `https://lemmy.world/u/lwCET`).
- **Lemmy Instance**: Provide the url of an Lemmy instance to follow all posts
of this instance (e.g. `https://lemmy.world`).
''';
/// The [AddSourceLemmy] widget is used to display the form to add a new Reddit
/// source.
class AddSourceLemmy extends StatefulWidget {
const AddSourceLemmy({
super.key,
required this.column,
});
final FDColumn column;
@override
State<AddSourceLemmy> createState() => _AddSourceLemmyState();
}
class _AddSourceLemmyState extends State<AddSourceLemmy> {
final _formKey = GlobalKey<FormState>();
final _lemmyController = TextEditingController();
bool _isLoading = false;
String _error = '';
/// [_addSource] adds a new Reddit source. The user can provide a subreddit or
/// a user. It is also possible to provide the complete RSS feed url.
Future<void> _addSource() async {
setState(() {
_isLoading = true;
_error = '';
});
try {
AppRepository app = Provider.of<AppRepository>(context, listen: false);
await app.addSource(
widget.column.id,
FDSourceType.lemmy,
FDSourceOptions(
lemmy: _lemmyController.text,
),
);
setState(() {
_isLoading = false;
_error = '';
});
if (mounted) {
Navigator.of(context).pop();
}
} on ApiException catch (err) {
setState(() {
_isLoading = false;
_error = 'Failed to add source: ${err.message}';
});
} catch (err) {
setState(() {
_isLoading = false;
_error = 'Failed to add source: ${err.toString()}';
});
}
}
@override
void dispose() {
_lemmyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AddSourceForm(
onTap: _addSource,
isLoading: _isLoading,
error: _error,
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
selectable: true,
data: _helpText,
onTapLink: (text, href, title) {
try {
if (href != null) {
openUrl(href);
}
} catch (_) {}
},
),
const SizedBox(
height: Constants.spacingMiddle,
),
TextFormField(
controller: _lemmyController,
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: true,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Lemmy Url',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),
),
);
}
}

View File

@@ -118,6 +118,7 @@ class _AddSourceMastodonState extends State<AddSourceMastodon> {
border: OutlineInputBorder(),
labelText: 'Username, Hashtag, or Url',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -123,6 +123,7 @@ class _AddSourceMediumState extends State<AddSourceMedium> {
border: OutlineInputBorder(),
labelText: 'Medium Url, Author or Tag',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -119,6 +119,7 @@ class _AddSourceNitterState extends State<AddSourceNitter> {
border: OutlineInputBorder(),
labelText: 'Username or Search Term',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:provider/provider.dart';
import 'package:feeddeck/models/column.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/utils/api_exception.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/openurl.dart';
import 'package:feeddeck/widgets/source/add/add_source_form.dart';
const _helpText = '''
The Pinterest source can be used to follow your favorite Pinterest users or
boards.
- **User**: `@username` or `https://www.pinterest.com/username/`
- **Board**: `@username/board` or `https://www.pinterest.com/username/board/`
''';
/// The [AddSourcePinterest] widget is used to display the form to add a new
/// Pinterest source.
class AddSourcePinterst extends StatefulWidget {
const AddSourcePinterst({
super.key,
required this.column,
});
final FDColumn column;
@override
State<AddSourcePinterst> createState() => _AddSourcePinterstState();
}
class _AddSourcePinterstState extends State<AddSourcePinterst> {
final _formKey = GlobalKey<FormState>();
final _pinterestController = TextEditingController();
bool _isLoading = false;
String _error = '';
/// [_addSource] adds a new Pinterst source. To add a new Pinterest source the
/// user must provide the URL of an user / a board or an url via the
/// [_pinterestController].
Future<void> _addSource() async {
setState(() {
_isLoading = true;
_error = '';
});
try {
AppRepository app = Provider.of<AppRepository>(context, listen: false);
await app.addSource(
widget.column.id,
FDSourceType.pinterest,
FDSourceOptions(
pinterest: _pinterestController.text,
),
);
setState(() {
_isLoading = false;
_error = '';
});
if (mounted) {
Navigator.of(context).pop();
}
} on ApiException catch (err) {
setState(() {
_isLoading = false;
_error = 'Failed to add source: ${err.message}';
});
} catch (err) {
setState(() {
_isLoading = false;
_error = 'Failed to add source: ${err.toString()}';
});
}
}
@override
void dispose() {
_pinterestController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AddSourceForm(
onTap: _addSource,
isLoading: _isLoading,
error: _error,
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
selectable: true,
data: _helpText,
onTapLink: (text, href, title) {
try {
if (href != null) {
openUrl(href);
}
} catch (_) {}
},
),
const SizedBox(
height: Constants.spacingMiddle,
),
TextFormField(
controller: _pinterestController,
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: true,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Pinterest Url, Username or Board',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),
),
);
}
}

View File

@@ -116,6 +116,7 @@ class _AddSourcePodcastState extends State<AddSourcePodcast> {
border: OutlineInputBorder(),
labelText: 'Podcast Url',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
@@ -8,6 +9,7 @@ import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/utils/api_exception.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/get_feed.dart';
import 'package:feeddeck/utils/openurl.dart';
import 'package:feeddeck/widgets/source/add/add_source_form.dart';
@@ -52,12 +54,29 @@ class _AddSourceRedditState extends State<AddSourceReddit> {
try {
AppRepository app = Provider.of<AppRepository>(context, listen: false);
/// To avoid getting rate limited by Reddit, we already fetch the feed
/// here and send it to the Supabase edge function within the [data]
/// field.
/// Since this only works for the desktop and mobile clients, we have to
/// check if the user is on the web, so that we can still try to fetch the
/// feed in the edge function.
final feedData = kIsWeb
? null
: await getFeed(
FDSourceType.reddit,
FDSourceOptions(
reddit: _redditController.text,
),
);
await app.addSource(
widget.column.id,
FDSourceType.reddit,
FDSourceOptions(
reddit: _redditController.text,
),
feedData,
);
setState(() {
_isLoading = false;
@@ -120,6 +139,7 @@ class _AddSourceRedditState extends State<AddSourceReddit> {
border: OutlineInputBorder(),
labelText: 'Reddit Url, Subreddit or User',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -14,6 +14,10 @@ import 'package:feeddeck/widgets/source/add/add_source_form.dart';
const _helpText = '''
The RSS source can be used to follow all your RSS feeds. You have to provide
the url for the RSS feed, e.g. `https://www.tagesschau.de/xml/rss2/`.
If you do not know the url of the RSS feed you can also provide the url of the
website you want to add and we try to find a RSS feed for you, e.g.
`https://www.tagesschau.de`.
''';
/// The [AddSourceRSS] widget is used to display the form to add a new RSS
@@ -118,6 +122,7 @@ class _AddSourceRSSState extends State<AddSourceRSS> {
border: OutlineInputBorder(),
labelText: 'RSS Feed',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -103,6 +103,7 @@ class _AddSourceStackOverflowState extends State<AddSourceStackOverflow> {
border: OutlineInputBorder(),
labelText: 'Url',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
];
@@ -120,6 +121,7 @@ class _AddSourceStackOverflowState extends State<AddSourceStackOverflow> {
border: OutlineInputBorder(),
labelText: 'Tag',
),
onFieldSubmitted: (value) => _addSource(),
),
const SizedBox(height: Constants.spacingMiddle),
DropdownButton<FDStackOverflowSort>(

View File

@@ -117,6 +117,7 @@ class _AddSourceTumblrState extends State<AddSourceTumblr> {
border: OutlineInputBorder(),
labelText: 'Blog',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -110,6 +110,7 @@ class _AddSourceXState extends State<AddSourceX> {
border: OutlineInputBorder(),
labelText: 'Username',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -115,6 +115,7 @@ class _AddSourceYouTubeState extends State<AddSourceYouTube> {
border: OutlineInputBorder(),
labelText: 'Channel Url',
),
onFieldSubmitted: (value) => _addSource(),
),
],
),

View File

@@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/fd_icons.dart';
import 'package:feeddeck/utils/image_url.dart';
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
/// [SourceIcon] can be used to show the image for a source. For that the [icon]
/// of the source must be provided. The size of the image can be adjusted via
@@ -18,14 +16,12 @@ class SourceIcon extends StatelessWidget {
super.key,
required this.type,
required this.icon,
this.iconType = FDImageType.source,
required this.size,
});
final FDSourceType type;
final String? icon;
final double size;
final FDImageType iconType;
/// buildIcon returns the provided [icon] with the provided [backgroundColor].
Widget buildIcon(
@@ -47,99 +43,21 @@ class SourceIcon extends StatelessWidget {
/// [buildDefaultIcon] returns an icon based on the provided source [type].
Widget buildDefaultIcon(double iconSize) {
switch (type) {
case FDSourceType.github:
return buildIcon(
FDIcons.github,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.googlenews:
return buildIcon(
FDIcons.googlenews,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.mastodon:
return buildIcon(
FDIcons.mastodon,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.medium:
return buildIcon(
FDIcons.medium,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.nitter:
return buildIcon(
FDIcons.nitter,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.podcast:
return buildIcon(
Icons.podcasts,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.reddit:
return buildIcon(
FDIcons.reddit,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.rss:
return buildIcon(
FDIcons.rss,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.stackoverflow:
return buildIcon(
FDIcons.stackoverflow,
iconSize,
type.color,
const Color(0xffffffff),
);
case FDSourceType.tumblr:
return buildIcon(
FDIcons.tumblr,
iconSize,
type.color,
const Color(0xffffffff),
);
// case FDSourceType.x:
// return buildIcon(
// FDIcons.x,
// iconSize,
// type.color,
// const Color(0xffffffff),
// );
case FDSourceType.youtube:
return buildIcon(
FDIcons.youtube,
iconSize,
type.color,
const Color(0xffffffff),
);
default:
return buildIcon(
FDIcons.feeddeck,
iconSize,
Constants.primary,
Constants.onPrimary,
);
if (FDSourceType.values.contains(type)) {
return buildIcon(
type.icon,
iconSize,
type.bgColor,
type.fgColor,
);
}
return buildIcon(
FDIcons.feeddeck,
iconSize,
Constants.primary,
Constants.onPrimary,
);
}
@override
@@ -155,7 +73,7 @@ class SourceIcon extends StatelessWidget {
height: size,
width: size,
fit: BoxFit.cover,
imageUrl: getImageUrl(iconType, icon!),
imageUrl: icon!,
placeholder: (context, url) => buildDefaultIcon(size),
errorWidget: (context, url, error) => buildDefaultIcon(size),
),

View File

@@ -0,0 +1,80 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart' as cni;
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:feeddeck/repositories/settings_repository.dart';
/// [getImageUrl] returns the "correct" image url for the provided [imageUrl].
/// "Correct" means that depending on the provided [imageUrl] and the current
/// platform, the image url will be pointed to the Supabase storage or will be
/// proxied via the "image-proxy-v1" Supabase function.
String getImageUrl(String imageUrl) {
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
if (kIsWeb) {
return '${SettingsRepository().supabaseUrl}/functions/v1/image-proxy-v1?media=${Uri.encodeQueryComponent(imageUrl)}';
}
return imageUrl;
}
return '${SettingsRepository().supabaseUrl}/storage/v1/object/public/sources/$imageUrl';
}
/// The [CachedNetworkImage] is a wrapper around the [cni.CachedNetworkImage]
/// widget, which will automatically use the correct url to display the image,
/// via the [getImageUrl] function.
class CachedNetworkImage extends StatelessWidget {
final String imageUrl;
final double? width;
final double? height;
final BoxFit? fit;
final Widget Function(BuildContext, String)? placeholder;
final Widget Function(BuildContext, String, Object)? errorWidget;
const CachedNetworkImage({
super.key,
required this.imageUrl,
this.width,
this.height,
this.fit,
this.placeholder,
this.errorWidget,
});
@override
Widget build(BuildContext context) {
return cni.CachedNetworkImage(
cacheManager: CustomCacheManager(),
imageUrl: getImageUrl(imageUrl),
width: width,
height: height,
fit: fit,
placeholder: placeholder,
errorWidget: errorWidget,
);
}
}
/// [CustomCacheManager] is a custom [CacheManager] which is used by the
/// [CachedNetworkImage] widget to cache the images. This is required to adjust
/// the `stalePeriod` to 7 days, instead of the default 30 days. 7 days should
/// be enough for our use case and will reduce the storage usage.
class CustomCacheManager extends CacheManager with ImageCacheManager {
static const key = 'libCachedImageData';
static final CustomCacheManager _instance = CustomCacheManager._internal();
factory CustomCacheManager() {
return _instance;
}
CustomCacheManager._internal()
: super(
Config(
key,
stalePeriod: const Duration(days: 7),
),
);
}

View File

@@ -50,6 +50,133 @@
</categories>
<releases>
<release version="v1.3.0" date="2024-02-12">
<description>
<p>Added</p>
<ul>
<li>#136: [nitter] Add Support for Piped Videos @ricoberger</li>
<li>#137: [landing] Add Feature Images for Mobile Screens @ricoberger</li>
<li>#135: [mastodon] Add Support for YouTube Videos @ricoberger</li>
<li>#134: [reddit] Add Support for YouTube Videos @ricoberger</li>
<li>#132: [core] Add Continuous Delivery Workflow for Linux arm64 @ricoberger</li>
<li>#128: [core] Make Log Level Configurable @ricoberger</li>
<li>#125: [core] Add Open Link Action @ricoberger</li>
<li>#118: [core] Add Client Side Scraping of Sources @ricoberger</li>
<li>#105: [core] Refactor Tools and add get-feed Tool @ricoberger</li>
<li>#101: [rss] Show Videos from RSS Feeds @ricoberger</li>
<li>#98: [core] Add Tests for Sources @ricoberger</li>
<li>#97: [core] Add Test Setup for Deno @ricoberger</li>
<li>#94: [lemmy] Add Support for Lemmy @ricoberger</li>
<li>#87: [core] Add Test Setup for Flutter @ricoberger</li>
</ul>
<p>Fixed</p>
<ul>
<li>#133: [core] Fix Password Validations @ricoberger</li>
<li>#124: [core] Add Missing SafeArea Widget @ricoberger</li>
<li>#96: [core] Fix Converting of HTML to Plain Text in Description @ricoberger</li>
<li>#93: [core] Fix Index Reset for Tabs in Small Deck Layout @ricoberger</li>
</ul>
<p>Changed</p>
<ul>
<li>#138: [core] Increase Update Interval for Reddit and Nitter @ricoberger</li>
<li>#131: [core] Update macOS GitHub Action Runners @ricoberger</li>
<li>#130: [core] Forbid Weak Passwords @ricoberger</li>
<li>#129: [rss] Do Not Remove HTML Tags @ricoberger</li>
<li>#127: Bump the docker group in /supabase/functions/_cmd with 1 update @dependabot</li>
<li>#126: Bump the npm-landing group in /landing with 2 updates @dependabot</li>
<li>#121: Bump the pub group in /app with 2 updates @dependabot</li>
<li>#120: Bump the npm-landing group in /landing with 1 update @dependabot</li>
<li>#123: [landing] Add Lemmy Icon @ricoberger</li>
<li>#122: [core] Fix Naming of Files @ricoberger</li>
<li>#119: Bump the pub group in /app with 9 updates @dependabot</li>
<li>#114: Bump the docker group in /supabase/functions/_cmd with 1 update @dependabot</li>
<li>#110: Bump the github-actions group with 4 updates @dependabot</li>
<li>#116: Bump the npm-landing group in /landing with 11 updates @dependabot</li>
<li>#113: [core] Add Assignees to Dependabot Configuration @ricoberger</li>
<li>#112: [core] Add Additional Headers for Web Deployment @ricoberger</li>
<li>#106: Update Flutter to Version 3.16.5 @ricoberger</li>
<li>#104: [core] Replace Deprecated serve Function @ricoberger</li>
<li>#103: [core] Improve Error Handling for Feed Edge Functions @ricoberger</li>
<li>#102: [core] Disable Right Click for Item Actions on Web @ricoberger</li>
<li>#100: [core] Update Deno Modules @ricoberger</li>
<li>#99: [rss] Parse Atom and RDF Feeds from Websites @ricoberger</li>
<li>#95: [core] Improve ItemVideoPlayer Widget @ricoberger</li>
<li>#89: Bump the npm-email-templates group in /supabase/email-templates with 1 update @dependabot</li>
<li>#90: Bump the pub group in /app with 1 update @dependabot</li>
<li>#91: Bump the npm-landing group in /landing with 7 updates @dependabot</li>
<li>#92: Bump the docker group in /supabase/functions/_cmd with 1 update @dependabot</li>
</ul>
</description>
<url>https://github.com/feeddeck/feeddeck/releases/tag/v1.2.1</url>
</release>
<release version="v1.2.1" date="2023-11-30">
<description>
<p>Fixed</p>
<ul>
<li>#84: [podcast] Stop Audio Playback on Windows and Linux @ricoberger</li>
<li>#82: [core] Fix build.gradle File for Android Release @ricoberger</li>
</ul>
<p>Changed</p>
<ul>
<li>#86: [core] Remove Blank Line in Item Preview Description @ricoberger</li>
<li>#85: [core] Add Right Click Support for Item Actions @ricoberger</li>
<li>#83: [medium] Extend Filter Words List @ricoberger</li>
</ul>
</description>
<url>https://github.com/feeddeck/feeddeck/releases/tag/v1.2.1</url>
</release>
<release version="v1.2.0" date="2023-11-26">
<description>
<p>Added</p>
<ul>
<li>#74: [pinterest] Add Support for Pinterest @ricoberger</li>
<li>#72: [rss] Allow Users to Provide a Website URL @ricoberger</li>
<li>#71: [core] Add GitHub Action for iOS and Android @ricoberger</li>
<li>#63: [core] Add Development Setup for Neovim @ricoberger</li>
<li>#56: [youtube] Add Desktop Support @ricoberger</li>
<li>#54: [core] Sign Out from Current Device @ricoberger</li>
<li>#51: [mastodon] Add Support for Videos @ricoberger</li>
</ul>
<p>Fixed</p>
<ul>
<li>#75: [core] Fix getMedia Function @ricoberger</li>
<li>#73: [core] Fix Decoding of Special Characters @ricoberger</li>
<li>#64: [github] Fix Icons in Item Preview @ricoberger</li>
<li>#58: [core] Add Missing Divider to Video Quality Selection @ricoberger</li>
<li>#57: [core] Fix Modal Bottom Sheet Size for Images @ricoberger</li>
<li>#55: [github] Fix Notification Links for PRs @ricoberger</li>
</ul>
<p>Changed</p>
<ul>
<li>#79: [medium] Remove Spam @ricoberger</li>
<li>#78: [reddit] Remove Tables from Description @ricoberger</li>
<li>#77: [core] Improve Subtitle in Details View @ricoberger</li>
<li>#76: [core] Improve Icon Handling @ricoberger</li>
<li>#70: [core] Update Flutter to Version 3.16.0 @ricoberger</li>
<li>#69: [core] Submit "Add Source" Forms on Enter @ricoberger</li>
<li>#68: Bump the pub group in /app with 8 updates @dependabot</li>
<li>#61: Bump the npm-email-templates group in /supabase/email-templates with 1 update @dependabot</li>
<li>#60: Bump the npm-landing group in /landing with 7 updates @dependabot</li>
<li>#62: Bump the github-actions group with 1 update @dependabot</li>
<li>#67: [core] Add Custom Cache Manager @ricoberger</li>
<li>#66: [core] Improve Media Handling @ricoberger</li>
<li>#65: [core] Run "deno fmt" @ricoberger</li>
<li>#53: [core] Improve Tabs Handling in Small Deck Layout @ricoberger</li>
<li>#52: [rss] Improve Rendering of Items @ricoberger</li>
</ul>
</description>
<url>https://github.com/feeddeck/feeddeck/releases/tag/v1.2.0</url>
</release>
<release version="v1.1.1" date="2023-10-21">
<description>
<p>Fixed</p>
@@ -66,7 +193,7 @@
<ul>
<li>#48: [core] Improve Android App Icons @ricoberger</li>
<li>#47: [core] Clear Cached Items @ricoberger</li>
<li>#39: [core] Add Privacy Policy and Terms & Conditions @ricoberger</li>
<li>#39: [core] Add Privacy Policy and Terms and Conditions @ricoberger</li>
<li>#38: [core] Enable In-App Purchases for Android @ricoberger</li>
</ul>
</description>

View File

@@ -6,15 +6,23 @@
#include "generated_plugin_registrant.h"
#include <gtk/gtk_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);

View File

@@ -3,7 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
gtk
media_kit_libs_linux
media_kit_video
screen_retriever
url_launcher_linux
window_manager

View File

@@ -9,14 +9,18 @@ import app_links
import audio_service
import audio_session
import just_audio
import media_kit_libs_macos_video
import media_kit_video
import package_info_plus
import path_provider_foundation
import purchases_flutter
import screen_brightness_macos
import screen_retriever
import shared_preferences_foundation
import sign_in_with_apple
import sqflite
import url_launcher_macos
import wakelock_plus
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -24,13 +28,17 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin"))
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View File

@@ -11,19 +11,25 @@ PODS:
- FMDB/standard (2.7.5)
- just_audio (0.0.1):
- FlutterMacOS
- media_kit_libs_macos_video (1.0.4):
- FlutterMacOS
- media_kit_native_event_loop (1.0.0):
- FlutterMacOS
- media_kit_video (0.0.1):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- purchases_flutter (6.0.0):
- purchases_flutter (6.19.0):
- FlutterMacOS
- PurchasesHybridCommon (= 9.3.0)
- PurchasesHybridCommon (9.3.0):
- RevenueCat (= 4.33.0)
- RevenueCat (4.33.0)
- screen_brightness_macos (0.1.0):
- FlutterMacOS
- PurchasesHybridCommon (= 7.0.0)
- PurchasesHybridCommon (7.0.0):
- RevenueCat (= 4.27.0)
- RevenueCat (4.27.0)
- screen_retriever (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
@@ -36,6 +42,8 @@ PODS:
- FMDB (>= 2.7.5)
- url_launcher_macos (0.0.1):
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
- window_manager (0.2.0):
- FlutterMacOS
@@ -45,15 +53,19 @@ DEPENDENCIES:
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- purchases_flutter (from `Flutter/ephemeral/.symlinks/plugins/purchases_flutter/macos`)
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
SPEC REPOS:
@@ -73,14 +85,20 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral
just_audio:
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/macos
media_kit_libs_macos_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_native_event_loop:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
media_kit_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
purchases_flutter:
:path: Flutter/ephemeral/.symlinks/plugins/purchases_flutter/macos
screen_brightness_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
screen_retriever:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
shared_preferences_foundation:
@@ -91,6 +109,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
window_manager:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
@@ -101,19 +121,23 @@ SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489
media_kit_native_event_loop: d20622d35dd6d06fe71223976bd70a2bcf595dce
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
purchases_flutter: 9aad80bf27960c38fdeafc27ab066cb55615aed5
PurchasesHybridCommon: af3b2413f9cb999bc1fdca44770bdaf39dfb89fa
RevenueCat: 84fbe2eb9bbf63e1abf346ccd3ff9ee45d633e3b
purchases_flutter: f9e17bbb58861b14c682598b555f123c30a112b1
PurchasesHybridCommon: 809461dbc8ff23b4dd0d5260c005b4017d6205b6
RevenueCat: 1512a074bebd78b7efb341ce1c33bfc8d292c53a
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sign_in_with_apple: a9e97e744e8edc36aefc2723111f652102a7a727
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
PODFILE CHECKSUM: 8d40c19d3cbdb380d870685c3a564c989f1efa52
COCOAPODS: 1.13.0
COCOAPODS: 1.15.0

View File

@@ -1,22 +1,30 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
app_links:
dependency: "direct main"
description:
name: app_links
sha256: eb83c2b15b78a66db04e95132678e910fcdb8dc3a9b0aed0c138f50b2bef0dae
sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
url: "https://pub.dev"
source: hosted
version: "3.4.5"
version: "3.5.0"
archive:
dependency: transitive
description:
name: archive
sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03"
sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
url: "https://pub.dev"
source: hosted
version: "3.4.6"
version: "3.4.9"
args:
dependency: transitive
description:
@@ -61,10 +69,10 @@ packages:
dependency: transitive
description:
name: audio_session
sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad"
sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f"
url: "https://pub.dev"
source: hosted
version: "0.1.16"
version: "0.1.18"
boolean_selector:
dependency: transitive
description:
@@ -77,26 +85,26 @@ packages:
dependency: "direct main"
description:
name: cached_network_image
sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
url: "https://pub.dev"
source: hosted
version: "3.3.0"
version: "3.3.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "4.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
carousel_slider:
dependency: "direct main"
description:
@@ -125,10 +133,10 @@ packages:
dependency: transitive
description:
name: cli_util
sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
url: "https://pub.dev"
source: hosted
version: "0.4.0"
version: "0.4.1"
clock:
dependency: transitive
description:
@@ -141,10 +149,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.17.2"
version: "1.18.0"
console:
dependency: transitive
description:
@@ -162,7 +170,7 @@ packages:
source: hosted
version: "3.1.1"
crypto:
dependency: transitive
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
@@ -185,6 +193,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.6"
dbus:
dependency: transitive
description:
name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
dio:
dependency: transitive
description:
name: dio
sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3"
url: "https://pub.dev"
source: hosted
version: "5.4.0"
fake_async:
dependency: transitive
description:
@@ -215,7 +239,7 @@ packages:
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
dependency: "direct main"
description:
name: flutter_cache_manager
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
@@ -234,26 +258,26 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "3.0.1"
flutter_markdown:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "8afc9a6aa6d8e8063523192ba837149dbf3d377a37c0b0fc579149a1fbd4a619"
sha256: "35108526a233cc0755664d445f8a6b4b61e6f8fe993b3658b80b4a26827fc196"
url: "https://pub.dev"
source: hosted
version: "0.6.18"
version: "0.6.18+2"
flutter_native_splash:
dependency: "direct main"
description:
name: flutter_native_splash
sha256: "91004565166dbbc7a85e7e99b84124a287839830ca957cfe45004793fe6fe69f"
sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
version: "2.3.10"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -276,10 +300,10 @@ packages:
dependency: transitive
description:
name: functions_client
sha256: "3b157b4d3ae9e38614fd80fab76d1ef1e0e39ff3412a45de2651f27cecb9d2d2"
sha256: "9a0ab83a525c8691a6724746e642de755a299afa04158807787364cd9e718001"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "2.0.0"
get_it:
dependency: transitive
description:
@@ -292,10 +316,18 @@ packages:
dependency: transitive
description:
name: gotrue
sha256: "15359f3b3824dbc8feab3b79d06daefe6f7163afb727e83602385e2d4b809902"
sha256: f40610bacf1074723354b0856a4f586508ffb075b799f72466f34e843133deb9
url: "https://pub.dev"
source: hosted
version: "1.12.4"
version: "2.5.0"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hive:
dependency: transitive
description:
@@ -313,7 +345,7 @@ packages:
source: hosted
version: "1.1.0"
html:
dependency: transitive
dependency: "direct main"
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
@@ -329,13 +361,13 @@ packages:
source: hosted
version: "1.3.1"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.2.0"
http_parser:
dependency: transitive
description:
@@ -364,10 +396,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.19.0"
js:
dependency: transitive
description:
@@ -388,42 +420,43 @@ packages:
dependency: "direct main"
description:
name: just_audio
sha256: "5ed0cd723e17dfd8cd4b0253726221e67f6546841ea4553635cf895061fc335b"
sha256: b607cd1a43bac03d85c3aaee00448ff4a589ef2a77104e3d409889ff079bf823
url: "https://pub.dev"
source: hosted
version: "0.9.35"
version: "0.9.36"
just_audio_background:
dependency: "direct main"
description:
name: just_audio_background
sha256: d290c9c450083aee40cc481e2cb4c088dcbca35961598970ea1b6a6f6c68ae13
sha256: "3454ffc97edfa1282b7f42759bfa8aa13d9114a24465f4101e0d3ae58a9327fb"
url: "https://pub.dev"
source: hosted
version: "0.0.1-beta.10"
version: "0.0.1-beta.11"
just_audio_media_kit:
dependency: "direct main"
description:
name: just_audio_media_kit
sha256: d6288e898bc5ed499a938c3cf1ea99eeca4264f9b6ef7bdf92ace3e8b804e259
url: "https://pub.dev"
source: hosted
path: "."
ref: HEAD
resolved-ref: "5980ceac7cc385baf2269eda2dcb024d3e5203cc"
url: "https://github.com/feeddeck/just_audio_media_kit.git"
source: git
version: "1.0.0"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df
sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1
url: "https://pub.dev"
source: hosted
version: "4.2.1"
version: "4.2.2"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13
sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70"
url: "https://pub.dev"
source: hosted
version: "0.4.8"
version: "0.4.9"
jwt_decode:
dependency: transitive
description:
@@ -436,10 +469,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "3.0.0"
logging:
dependency: transitive
description:
@@ -473,13 +506,29 @@ packages:
source: hosted
version: "0.5.0"
media_kit:
dependency: transitive
dependency: "direct main"
description:
name: media_kit
sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a"
url: "https://pub.dev"
source: hosted
version: "1.1.10+1"
media_kit_libs_android_video:
dependency: transitive
description:
name: media_kit_libs_android_video
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
url: "https://pub.dev"
source: hosted
version: "1.3.6"
media_kit_libs_ios_video:
dependency: transitive
description:
name: media_kit_libs_ios_video
sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991
url: "https://pub.dev"
source: hosted
version: "1.1.4"
media_kit_libs_linux:
dependency: transitive
description:
@@ -488,11 +537,27 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.3"
media_kit_libs_windows_audio:
media_kit_libs_macos_video:
dependency: transitive
description:
name: media_kit_libs_windows_audio
sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53
name: media_kit_libs_macos_video
sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d
url: "https://pub.dev"
source: hosted
version: "1.1.4"
media_kit_libs_video:
dependency: "direct main"
description:
name: media_kit_libs_video
sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
media_kit_libs_windows_video:
dependency: transitive
description:
name: media_kit_libs_windows_video
sha256: "7bace5f35d9afcc7f9b5cdadb7541d2191a66bb3fc71bfa11c1395b3360f6122"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
@@ -504,14 +569,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
media_kit_video:
dependency: "direct main"
description:
name: media_kit_video
sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882
url: "https://pub.dev"
source: hosted
version: "1.2.4"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
mime:
dependency: transitive
description:
@@ -524,10 +597,10 @@ packages:
dependency: "direct dev"
description:
name: msix
sha256: "6e76e2491d5c809d784ce2b68e6c3426097fb5c68e61fe121c8c3341ab89bf46"
sha256: "519b183d15dc9f9c594f247e2d2339d855cf0eaacc30e19b128e14f3ecc62047"
url: "https://pub.dev"
source: hosted
version: "3.16.4"
version: "3.16.7"
nested:
dependency: transitive
description:
@@ -556,10 +629,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a"
sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "5.0.1"
package_info_plus_platform_interface:
dependency: transitive
description:
@@ -588,10 +661,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
path_provider_foundation:
dependency: transitive
description:
@@ -628,10 +701,18 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "6.0.2"
piped_client:
dependency: "direct main"
description:
name: piped_client
sha256: "8b96e1f9d8533c1da7eff7fbbd4bf188256fc76a20900d378b52be09418ea771"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
platform:
dependency: transitive
description:
@@ -644,10 +725,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
url: "https://pub.dev"
source: hosted
version: "2.1.6"
version: "2.1.7"
pointycastle:
dependency: transitive
description:
@@ -660,18 +741,18 @@ packages:
dependency: transitive
description:
name: postgrest
sha256: "87e35d3a59e327188321befbfbfcc5a7a2e71f0d0a13d975cbc7d169387ec712"
sha256: "748ebffffb60b4eaa270955dcf3742a19a2b315344c41ff1b4a0ebcd322b5181"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
version: "2.1.0"
provider:
dependency: "direct main"
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
url: "https://pub.dev"
source: hosted
version: "6.0.5"
version: "6.1.1"
pub_semver:
dependency: transitive
description:
@@ -684,18 +765,18 @@ packages:
dependency: "direct main"
description:
name: purchases_flutter
sha256: "3fb05df9d4ec901547c447a27830ce24d0f8b90e8f751513429479091385233d"
sha256: "27bef8c37c9863a0d280ed4668950fc1a17f36807f80c596c280795c8613ee07"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
version: "6.19.0"
realtime_client:
dependency: transitive
description:
name: realtime_client
sha256: d93f99b6ee42a7b7af3e15ef2965576172ff196426aabca24b91842fb27df116
sha256: "5831636c19802ba936093a35a7c5b745b130e268fa052e84b4b5290139d2ae03"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "2.0.0"
retry:
dependency: transitive
description:
@@ -720,6 +801,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
screen_brightness:
dependency: transitive
description:
name: screen_brightness
sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
screen_brightness_android:
dependency: transitive
description:
name: screen_brightness_android
sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf"
url: "https://pub.dev"
source: hosted
version: "0.1.0+2"
screen_brightness_ios:
dependency: transitive
description:
name: screen_brightness_ios
sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_macos:
dependency: transitive
description:
name: screen_brightness_macos
sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
screen_brightness_platform_interface:
dependency: transitive
description:
name: screen_brightness_platform_interface
sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_windows:
dependency: transitive
description:
name: screen_brightness_windows
sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
screen_retriever:
dependency: transitive
description:
@@ -780,10 +909,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_web
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
shared_preferences_windows:
dependency: transitive
description:
@@ -793,7 +922,7 @@ packages:
source: hosted
version: "2.3.2"
sign_in_with_apple:
dependency: transitive
dependency: "direct main"
description:
name: sign_in_with_apple
sha256: "0975c23b9f8b30a80e27d5659a75993a093d4cb5f4eb7d23a9ccc586fea634e0"
@@ -849,34 +978,34 @@ packages:
dependency: transitive
description:
name: sqflite_common
sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a"
sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.5.0+2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.11.1"
storage_client:
dependency: transitive
description:
name: storage_client
sha256: "7860281c718983a7cd388b2a87b45af495174701a0230cce2111b81a38352422"
sha256: bf5589d5de61a2451edb1b8960a0e673d4bb5c42ecc4dddf7c051a93789ced34
url: "https://pub.dev"
source: hosted
version: "1.5.3"
version: "2.0.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
string_scanner:
dependency: transitive
description:
@@ -889,26 +1018,26 @@ packages:
dependency: transitive
description:
name: supabase
sha256: "3d70f8a5d7a09916e1f8aa85d6bf548f8b674e18378498d79fecbfe09e825372"
sha256: "4bce9c49f264f4cd44b4ffc895647af2dca0c40125c169045be9f708fd2a2a40"
url: "https://pub.dev"
source: hosted
version: "1.11.9"
version: "2.0.7"
supabase_flutter:
dependency: "direct main"
description:
name: supabase_flutter
sha256: "8794dd3b292ebed40ec920f6ef303cb2d78f927a9cff00eebd776c9fa9862153"
sha256: "32597ffe9993bc47bc5a2020421b1940e634ea4293cff80385fa67fb9fff46d6"
url: "https://pub.dev"
source: hosted
version: "1.10.22"
version: "2.3.2"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.0+1"
term_glyph:
dependency: transitive
description:
@@ -921,18 +1050,18 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.6.1"
timeago:
dependency: "direct main"
description:
name: timeago
sha256: "4addcda362e51f23cf7ae2357fccd053f29d59b4ddd17fb07fc3e7febb47a456"
sha256: d3204eb4c788214883380253da7f23485320a58c11d145babc82ad16bf4e7764
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "3.6.1"
tint:
dependency: transitive
description:
@@ -977,74 +1106,74 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
url: "https://pub.dev"
source: hosted
version: "6.1.14"
version: "6.2.4"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.2.0"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3
url: "https://pub.dev"
source: hosted
version: "6.1.5"
version: "6.2.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
url: "https://pub.dev"
source: hosted
version: "3.0.7"
version: "3.1.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.2.0"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9"
url: "https://pub.dev"
source: hosted
version: "2.0.20"
version: "2.2.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
version: "3.0.8"
version: "3.1.1"
uuid:
dependency: transitive
description:
name: uuid
sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.2.2"
vector_math:
dependency: transitive
description:
@@ -1053,14 +1182,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
volume_controller:
dependency: transitive
description:
name: volume_controller
sha256: "189bdc7a554f476b412e4c8b2f474562b09d74bc458c23667356bce3ca1d48c9"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
wakelock_plus:
dependency: transitive
description:
name: wakelock_plus
sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d
url: "https://pub.dev"
source: hosted
version: "1.1.4"
wakelock_plus_platform_interface:
dependency: transitive
description:
name: wakelock_plus_platform_interface
sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
version: "0.3.0"
web_socket_channel:
dependency: transitive
description:
@@ -1069,54 +1222,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.0"
webview_flutter:
dependency: transitive
description:
name: webview_flutter
sha256: c1ab9b81090705c6069197d9fdc1625e587b52b8d70cdde2339d177ad0dbb98e
url: "https://pub.dev"
source: hosted
version: "4.4.1"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff
url: "https://pub.dev"
source: hosted
version: "3.12.0"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "30b9af6bdd457b44c08748b9190d23208b5165357cc2eb57914fee1366c42974"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
win32:
dependency: transitive
description:
name: win32
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
url: "https://pub.dev"
source: hosted
version: "5.0.9"
version: "5.1.1"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7
sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494
url: "https://pub.dev"
source: hosted
version: "0.3.7"
version: "0.3.8"
xdg_directories:
dependency: transitive
description:
@@ -1129,10 +1250,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.5.0"
yaml:
dependency: transitive
description:
@@ -1145,26 +1266,18 @@ packages:
dependency: transitive
description:
name: yet_another_json_isolate
sha256: "86fad76026c4241a32831d6c7febd8f9bded5019e2cd36c5b148499808d8307d"
sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069
url: "https://pub.dev"
source: hosted
version: "1.1.1"
youtube_player_iframe:
version: "2.0.0"
youtube_explode_dart:
dependency: "direct main"
description:
name: youtube_player_iframe
sha256: d7aec9083430db4e5da83a3b5d7b7fcbb93cfa027d9f680ce3c7e7cd20724305
name: youtube_explode_dart
sha256: "77a55747579c76b5d071bca3941cfca141207f064b3f0322994573cb4a0c2831"
url: "https://pub.dev"
source: hosted
version: "4.0.4"
youtube_player_iframe_web:
dependency: transitive
description:
name: youtube_player_iframe_web
sha256: c7020816031600349b56d2729d4e8be011fcb723ff7dc2dd0cdf72096a0e5ff4
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "2.1.0"
sdks:
dart: ">=3.1.3 <4.0.0"
flutter: ">=3.13.0"
dart: ">=3.2.6 <4.0.0"
flutter: ">=3.16.0"

View File

@@ -16,10 +16,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.1.1+7
version: 1.3.0+10
environment:
sdk: '>=3.1.3 <4.0.0'
sdk: '>=3.2.6 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -38,27 +38,42 @@ dependencies:
cupertino_icons: ^1.0.2
app_links: ^3.4.3
cached_network_image: ^3.2.3
cached_network_image: ^3.3.1
carousel_slider: ^4.2.1
collection: ^1.17.0
crypto: ^3.0.3
flutter_cache_manager: ^3.3.1
flutter_markdown: ^0.6.14
flutter_native_splash: ^2.2.19
flutter_native_splash: ^2.3.10
html: ^0.15.4
html2md: ^1.2.6
intl: ^0.18.1
http: ^1.2.0
intl: ^0.19.0
just_audio: ^0.9.32
just_audio_background: ^0.0.1-beta.10
just_audio_media_kit: ^1.0.0
package_info_plus: ^4.1.0
# We use our own fork of the "just_audio_media_kit" package, where we
# replaced the "media_kit_libs_windows_audio" with
# "media_kit_libs_windows_video" package, so that we can play video files on
# Windows via the "media_kit" package.
just_audio_media_kit:
git:
url: https://github.com/feeddeck/just_audio_media_kit.git
media_kit: ^1.1.10+1
media_kit_video: ^1.2.4
media_kit_libs_video: ^1.0.4
package_info_plus: ^5.0.1
piped_client: ^0.1.0
provider: ^6.0.4
purchases_flutter: ^6.0.0
purchases_flutter: ^6.19.0
rxdart: ^0.27.7
scroll_to_index: ^3.0.1
shared_preferences: ^2.1.0
supabase_flutter: ^1.10.6
timeago: ^3.4.0
url_launcher: ^6.1.10
window_manager: ^0.3.4
youtube_player_iframe: ^4.0.4
sign_in_with_apple: ^5.0.0
supabase_flutter: ^2.3.2
timeago: ^3.6.0
url_launcher: ^6.2.4
window_manager: ^0.3.8
youtube_explode_dart: ^2.1.0
dev_dependencies:
flutter_test:
@@ -69,11 +84,11 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
flutter_lints: ^3.0.1
flutter_launcher_icons: ^0.13.1
import_sorter: ^4.6.0
msix: ^3.16.1
msix: ^3.16.7
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -160,7 +175,7 @@ msix_config:
publisher_display_name: Rico Berger
identity_name: 26077RicoBerger.FeedDeck
publisher: CN=7740451A-C179-450A-B346-7231CA231332
msix_version: 1.1.1.0
msix_version: 1.3.0.0
logo_path: templates/app-icon/windows.png
languages: en-us
capabilities: internetClient

58
app/run.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
if [[ "$#" -lt 1 ]]; then
echo "$(basename "$0") -- program to run the app
where:
-e|--environment set the environment on which the app should be run, e.g. -e=\"local\"
-d|--device set the device on which the app should be run, e.g. -d=\"chrome\""
exit
fi
for arg in "$@"
do
case ${arg} in
-e=*|--environment=*)
environment="${arg#*=}"
shift
;;
-d=*|--device=*)
device="${arg#*=}"
shift
;;
*)
;;
esac
done
if [ -z "${environment}" ] || [ -z "${device}" ]; then
echo "You have to provide an environment and a device"
echo " Example: $0 -e=\"local\" -d=\"chrome\""
echo " Provided: $0 -e=\"${environment}\" -d=\"${device}\""
exit 1
fi
echo "Run: $0 -e=\"${environment}\" -d=\"${device}\""
# Load the environment variables from the corresponding ".env" file in the
# "supabase" directory.
set -o allexport && source "../supabase/.env.${environment}" && set +o allexport
# When the local environment is used, we have to adjust the Supabase URL to the
# local Supabase instance, since the environment variable set by the .env file
# uses the Docker address of the Supabase instance.
supabase_url=$FEEDDECK_SUPABASE_URL
if [ "${environment}" == "local" ]; then
supabase_url="http://localhost:54321"
fi
# If the selected device is "chrome" we have to add some additional flags to the
# "flutter run" command, since we want to run the app always on the same port
# and we want to disable the web security.
additional_flags=""
if [ "${device}" == "chrome" ]; then
additional_flags="--web-port 3000 --web-browser-flag=--disable-web-security"
fi
# Run the app on the provided device and environment.
flutter run -d "${device}" ${additional_flags} --dart-define SUPABASE_URL=${supabase_url} --dart-define SUPABASE_ANON_KEY=${FEEDDECK_SUPABASE_ANON_KEY} --dart-define SUPABASE_SITE_URL=${FEEDDECK_SUPABASE_SITE_URL} --dart-define GOOGLE_CLIENT_ID=${FEEDDECK_GOOGLE_CLIENT_ID}

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