mirror of
https://github.com/feeddeck/feeddeck.git
synced 2026-03-09 23:22:02 -05:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b84bab217 | ||
|
|
1a56a8996e | ||
|
|
37bcc5e026 | ||
|
|
6a158f5176 | ||
|
|
0b7ca6cb14 | ||
|
|
303f78c3bc | ||
|
|
c0c87e2c10 | ||
|
|
babce57c80 | ||
|
|
ca5866ac13 | ||
|
|
508e255c8b | ||
|
|
aeeea4fd95 | ||
|
|
6029ee539e | ||
|
|
c9a596111c | ||
|
|
b8a73cc003 | ||
|
|
817eb4d9e8 | ||
|
|
20e3e736c2 | ||
|
|
20352c0301 | ||
|
|
e7b7000f46 | ||
|
|
fb3bec623a | ||
|
|
976c066004 | ||
|
|
304a9744a9 | ||
|
|
ee11cae8dc | ||
|
|
04314f116d | ||
|
|
b645244378 | ||
|
|
110ff56aa1 | ||
|
|
bbbeb9524f | ||
|
|
90fc7532ba | ||
|
|
fac622ef97 | ||
|
|
e29b94a576 | ||
|
|
911b3691b3 | ||
|
|
3a84376223 | ||
|
|
0f5a8e44f1 | ||
|
|
5753fb2714 | ||
|
|
4008660a35 | ||
|
|
1cb58e1e0f | ||
|
|
08e9170a80 | ||
|
|
4198a5bac6 | ||
|
|
295ae13705 | ||
|
|
0894f0e777 | ||
|
|
2966ecc651 | ||
|
|
49c168b5b2 | ||
|
|
8e0017e928 | ||
|
|
982add8fbb | ||
|
|
8065e19c85 | ||
|
|
9e59439226 | ||
|
|
5087c299d3 | ||
|
|
bddf5874d4 | ||
|
|
6c469e5d0d | ||
|
|
8c88ece3dc | ||
|
|
eb28a44cc8 | ||
|
|
240e9e93d9 | ||
|
|
eebec73fd2 | ||
|
|
1b226791b4 | ||
|
|
ad9885ce92 | ||
|
|
16418ab205 | ||
|
|
d9d82a1679 | ||
|
|
ff52516324 | ||
|
|
abd3c24f68 | ||
|
|
04ef618295 | ||
|
|
a58c93be8a | ||
|
|
5a8d6b34c1 | ||
|
|
9233e4d373 | ||
|
|
6e50af16a7 | ||
|
|
046479071b | ||
|
|
ffbed73669 | ||
|
|
03371cf645 | ||
|
|
eb606b5f6c | ||
|
|
df927516b1 | ||
|
|
fa23e095e5 | ||
|
|
2298176c3b | ||
|
|
3f7caf4ad4 | ||
|
|
db9363e7af | ||
|
|
ce36761c64 | ||
|
|
4e38cfdb5c | ||
|
|
1c7c88a9ca | ||
|
|
c7d208de23 | ||
|
|
a7639344a1 | ||
|
|
ff76531962 | ||
|
|
dc77f16a34 | ||
|
|
bcd03e7e60 | ||
|
|
92bea5d715 | ||
|
|
37cd4dff6f | ||
|
|
d62bf10eaf | ||
|
|
3afbe5674b | ||
|
|
6d5a699db6 | ||
|
|
e19885a594 | ||
|
|
6e27eb751c | ||
|
|
8e3586f315 | ||
|
|
e994ab6214 | ||
|
|
4906f9dc27 | ||
|
|
55c6da07d9 | ||
|
|
8dc83a5d5a | ||
|
|
94d5732f6a | ||
|
|
f4a9f84061 |
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -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:
|
||||
|
||||
165
.github/workflows/continuous-delivery.yaml
vendored
165
.github/workflows/continuous-delivery.yaml
vendored
@@ -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 }}
|
||||
|
||||
53
.github/workflows/continuous-integration.yaml
vendored
Normal file
53
.github/workflows/continuous-integration.yaml
vendored
Normal 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
|
||||
10
.github/workflows/release.yaml
vendored
10
.github/workflows/release.yaml
vendored
@@ -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
7
.gitignore
vendored
@@ -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
17
.vscode/settings.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
166
CONTRIBUTING.md
166
CONTRIBUTING.md
@@ -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
1
app/.gitignore
vendored
@@ -31,6 +31,7 @@ migrate_working_dir/
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
app/devtools_options.yaml
Normal file
1
app/devtools_options.yaml
Normal file
@@ -0,0 +1 @@
|
||||
extensions:
|
||||
Binary file not shown.
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
12
app/lib/repositories/layout_repository.dart
Normal file
12
app/lib/repositories/layout_repository.dart
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
55
app/lib/utils/get_feed.dart
Normal file
55
app/lib/utils/get_feed.dart
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
33
app/lib/utils/signin_with_apple.dart
Normal file
33
app/lib/utils/signin_with_apple.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
81
app/lib/widgets/item/details/item_details_lemmy.dart
Normal file
81
app/lib/widgets/item/details/item_details_lemmy.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
40
app/lib/widgets/item/details/item_details_pinterest.dart
Normal file
40
app/lib/widgets/item/details/item_details_pinterest.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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+'), ' '),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,10 @@ class ItemTitle extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (itemTitle.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return SelectableText(
|
||||
itemTitle,
|
||||
textAlign: TextAlign.left,
|
||||
|
||||
290
app/lib/widgets/item/details/utils/item_videos.dart
Normal file
290
app/lib/widgets/item/details/utils/item_videos.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
71
app/lib/widgets/item/preview/item_preview_lemmy.dart
Normal file
71
app/lib/widgets/item/preview/item_preview_lemmy.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
50
app/lib/widgets/item/preview/item_preview_pinterest.dart
Normal file
50
app/lib/widgets/item/preview/item_preview_pinterest.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class ItemPreviewRSS extends StatelessWidget {
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.plain,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.plain,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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+'), ' '),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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/',
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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>(
|
||||
|
||||
130
app/lib/widgets/source/add/add_source_lemmy.dart
Normal file
130
app/lib/widgets/source/add/add_source_lemmy.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,7 @@ class _AddSourceMastodonState extends State<AddSourceMastodon> {
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Username, Hashtag, or Url',
|
||||
),
|
||||
onFieldSubmitted: (value) => _addSource(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -123,6 +123,7 @@ class _AddSourceMediumState extends State<AddSourceMedium> {
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Medium Url, Author or Tag',
|
||||
),
|
||||
onFieldSubmitted: (value) => _addSource(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -119,6 +119,7 @@ class _AddSourceNitterState extends State<AddSourceNitter> {
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Username or Search Term',
|
||||
),
|
||||
onFieldSubmitted: (value) => _addSource(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
128
app/lib/widgets/source/add/add_source_pinterest.dart
Normal file
128
app/lib/widgets/source/add/add_source_pinterest.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,7 @@ class _AddSourcePodcastState extends State<AddSourcePodcast> {
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Podcast Url',
|
||||
),
|
||||
onFieldSubmitted: (value) => _addSource(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -117,6 +117,7 @@ class _AddSourceTumblrState extends State<AddSourceTumblr> {
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Blog',
|
||||
),
|
||||
onFieldSubmitted: (value) => _addSource(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -110,6 +110,7 @@ class _AddSourceXState extends State<AddSourceX> {
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Username',
|
||||
),
|
||||
onFieldSubmitted: (value) => _addSource(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -115,6 +115,7 @@ class _AddSourceYouTubeState extends State<AddSourceYouTube> {
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Channel Url',
|
||||
),
|
||||
onFieldSubmitted: (value) => _addSource(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
80
app/lib/widgets/utils/cached_network_image.dart
Normal file
80
app/lib/widgets/utils/cached_network_image.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
gtk
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
screen_retriever
|
||||
url_launcher_linux
|
||||
window_manager
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
447
app/pubspec.lock
447
app/pubspec.lock
@@ -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"
|
||||
|
||||
@@ -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
58
app/run.sh
Executable 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
Reference in New Issue
Block a user