Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8989442fc1 | ||
|
|
befe5b5977 | ||
|
|
11585f284c | ||
|
|
9503943b71 | ||
|
|
d38cf80818 | ||
|
|
3e03f15825 | ||
|
|
99e0538596 | ||
|
|
2a209519e1 | ||
|
|
461dc6f384 | ||
|
|
409aa51f1c | ||
|
|
3620fcb5b0 | ||
|
|
ca7c22b0d7 | ||
|
|
318aeb4187 | ||
|
|
1fb3bf63da | ||
|
|
aa5c4861db | ||
|
|
a7c5afdd83 | ||
|
|
38fc655a53 | ||
|
|
4c34d33ac0 | ||
|
|
eb9f84f650 | ||
|
|
443e2db167 | ||
|
|
347c570cde | ||
|
|
f8e2fa6128 | ||
|
|
9dbb979764 | ||
|
|
062db5d13a | ||
|
|
788c7c9440 | ||
|
|
aed602c55e | ||
|
|
24f4648274 | ||
|
|
d29d601513 | ||
|
|
f21d38b080 | ||
|
|
a0c64508c9 | ||
|
|
11084f04e9 | ||
|
|
2a8aa5f3c1 | ||
|
|
c64b8026ae | ||
|
|
ed76d85fd8 | ||
|
|
b2b3723ba8 | ||
|
|
ca9ce54061 | ||
|
|
8199c451b1 | ||
|
|
66173d5a38 | ||
|
|
a5fba341d3 | ||
|
|
689d3bd39b | ||
|
|
0b077ae973 | ||
|
|
9a6bb033bf | ||
|
|
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 |
19
.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,16 +37,9 @@ updates:
|
||||
directory: "/landing"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
groups:
|
||||
npm-landing:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/supabase/email-templates"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
npm-email-templates:
|
||||
npm:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
112
.github/workflows/continuous-delivery.yaml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: ./supabase/functions
|
||||
@@ -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.16.0'
|
||||
flutter-version: '3.22.2'
|
||||
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.16.0'
|
||||
flutter-version: '3.22.2'
|
||||
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.16.0'
|
||||
flutter-version: '3.22.2'
|
||||
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.22.2'
|
||||
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.16.0'
|
||||
flutter-version: '3.22.2'
|
||||
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
|
||||
@@ -369,7 +442,7 @@ jobs:
|
||||
# app works. The artifact of the build isn't uploaded / used.
|
||||
ios:
|
||||
name: iOS
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
defaults:
|
||||
run:
|
||||
@@ -382,7 +455,7 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
@@ -411,10 +484,13 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
|
||||
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.22.2'
|
||||
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
|
||||
8
.github/workflows/release.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Update Changelog
|
||||
uses: release-drafter/release-drafter@v5
|
||||
uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
config-name: release.yaml
|
||||
disable-autolabeler: true
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
cache-dependency-path: landing/package-lock.json
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- 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
|
||||
|
||||
10
.gitignore
vendored
@@ -1,11 +1,11 @@
|
||||
# Visual Studio Code Launch Configurations
|
||||
.vscode/launch.json
|
||||
|
||||
# Neovim
|
||||
.nvim.lua
|
||||
# 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
@@ -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"
|
||||
}
|
||||
}
|
||||
192
CONTRIBUTING.md
@@ -57,16 +57,16 @@ check your installed version:
|
||||
```sh
|
||||
$ flutter --version
|
||||
|
||||
Flutter 3.16.0 • channel stable • https://github.com/flutter/flutter.git
|
||||
Framework • revision db7ef5bf9f (9 days ago) • 2023-11-15 11:25:44 -0800
|
||||
Engine • revision 74d16627b9
|
||||
Tools • Dart 3.2.0 • DevTools 2.28.2
|
||||
Flutter 3.22.2 • channel stable • https://github.com/flutter/flutter.git
|
||||
Framework • revision 761747bfc5 (3 days ago) • 2024-06-05 22:15:13 +0200
|
||||
Engine • revision edd8546116
|
||||
Tools • Dart 3.4.3 • DevTools 2.34.3
|
||||
|
||||
$ 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
|
||||
@@ -79,131 +79,25 @@ required variables to the `flutter run` command:
|
||||
./run.sh --device="chrome" --environment="local"
|
||||
```
|
||||
|
||||
Alternative you can also run the project from Visual Studio Code or Neovim
|
||||
with the following configuration files. 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>`.
|
||||
To run the tests the following command can be used:
|
||||
|
||||
<details>
|
||||
<summary>Visual Studio Code: `.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
|
||||
flutter test
|
||||
```
|
||||
|
||||
</details>
|
||||
To check the test coverage the `--coverage` flag can be added to the command and
|
||||
an HTML report can be generated:
|
||||
|
||||
<details>
|
||||
<summary>Neovim: `.nvim.lua`</summary>
|
||||
```sh
|
||||
flutter test --coverage
|
||||
|
||||
# To generate the HTML report lcov is required, which can be installed via Homebrew:
|
||||
brew install lcov
|
||||
|
||||
```lua
|
||||
require('flutter-tools').setup_project({
|
||||
{
|
||||
name = 'Local - Chrome',
|
||||
flavor = 'debug',
|
||||
target = 'lib/main.dart',
|
||||
device = 'chrome',
|
||||
dart_define = {
|
||||
SUPABASE_URL = 'http://localhost:54321',
|
||||
SUPABASE_ANON_KEY = '<SUPABASE_ANON_KEY'
|
||||
SUPABASE_SITE_URL = '<SUPABASE_SITE_URL>',
|
||||
GOOGLE_CLIENT_ID = '<GOOGLE_CLIENT_ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
name = 'Local - iOS Simulator',
|
||||
flavor = 'debug',
|
||||
target = 'lib/main.dart',
|
||||
device = 'iPhone 14 Pro Max',
|
||||
dart_define = {
|
||||
SUPABASE_URL = 'http://localhost:54321',
|
||||
SUPABASE_ANON_KEY = '<SUPABASE_ANON_KEY'
|
||||
SUPABASE_SITE_URL = '<SUPABASE_SITE_URL>',
|
||||
GOOGLE_CLIENT_ID = '<GOOGLE_CLIENT_ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
name = 'Local - Chrome',
|
||||
flavor = 'debug',
|
||||
target = 'lib/main.dart',
|
||||
device = 'macOS',
|
||||
dart_define = {
|
||||
SUPABASE_URL = 'http://localhost:54321',
|
||||
SUPABASE_ANON_KEY = '<SUPABASE_ANON_KEY'
|
||||
SUPABASE_SITE_URL = '<SUPABASE_SITE_URL>',
|
||||
GOOGLE_CLIENT_ID = '<GOOGLE_CLIENT_ID',
|
||||
},
|
||||
},
|
||||
})
|
||||
genhtml coverage/lcov.info -o coverage/html
|
||||
open coverage/html/index.html
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Sort all Imports
|
||||
|
||||
To sort all imports in the Dart code in a uniformly way you have to run the
|
||||
@@ -336,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
|
||||
@@ -357,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
|
||||
@@ -458,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.
|
||||
|
||||
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Rico Berger
|
||||
Copyright (c) 2024 Rico Berger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -31,8 +31,8 @@ platforms. FeedDeck is written in [Flutter](https://flutter.dev/) and uses
|
||||
- **RSS and Social Media Feeds:** Follow your favorite RSS and social media
|
||||
feeds.
|
||||
- **News:** Get the latest news from your favorite RSS feeds and Google News.
|
||||
- **Social Media:** Follow your friends and favorite topics on Medium, Nitter,
|
||||
Reddit, Tumblr and X.
|
||||
- **Social Media:** Follow your friends and favorite topics on Medium, Reddit
|
||||
and Tumblr.
|
||||
- **GitHub:** Get your GitHub notifications and follow your repository
|
||||
activities.
|
||||
- **Podcasts:** Follow and listen to your favorite podcasts, via the built-in
|
||||
|
||||
1
app/.gitignore
vendored
@@ -31,6 +31,7 @@ migrate_working_dir/
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
@@ -12,11 +18,6 @@ if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
@@ -27,21 +28,18 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
namespace "app.feeddeck.feeddeck"
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -55,7 +53,7 @@ android {
|
||||
// minSdkVersion flutter.minSdkVersion
|
||||
// targetSdkVersion flutter.targetSdkVersion
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@@ -71,7 +69,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (project.hasProperty("keyStoreFile")) {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
// For testing purposes we sign with dummy credentials if no key properties are given.
|
||||
@@ -86,6 +84,5 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.google.android.gms:play-services-auth:20.6.0'
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}
|
||||
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.1.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -8,9 +8,6 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
@@ -24,12 +21,12 @@ PODS:
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- purchases_flutter (6.4.0):
|
||||
- purchases_flutter (6.30.2):
|
||||
- Flutter
|
||||
- PurchasesHybridCommon (= 8.0.0)
|
||||
- PurchasesHybridCommon (8.0.0):
|
||||
- RevenueCat (= 4.30.5)
|
||||
- RevenueCat (4.30.5)
|
||||
- PurchasesHybridCommon (= 11.1.0)
|
||||
- PurchasesHybridCommon (11.1.0):
|
||||
- RevenueCat (= 4.43.2)
|
||||
- RevenueCat (4.43.2)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -39,15 +36,13 @@ PODS:
|
||||
- Flutter
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
@@ -65,15 +60,13 @@ DEPENDENCIES:
|
||||
- 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`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- PurchasesHybridCommon
|
||||
- RevenueCat
|
||||
|
||||
@@ -109,41 +102,37 @@ EXTERNAL SOURCES:
|
||||
sign_in_with_apple:
|
||||
:path: ".symlinks/plugins/sign_in_with_apple/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
|
||||
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: a428f3e8ac54dfb499ff190efa99d6701094bc32
|
||||
PurchasesHybridCommon: 80262c5ffe6621e3cf3812e6103170f6d7fbcb79
|
||||
RevenueCat: c1e33f4e1f1fd239ba461652f02928e220becc31
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
purchases_flutter: 42d5544e7730ea89a88cc2f008b7c700fd147052
|
||||
PurchasesHybridCommon: 4022d5944cb30ec44ba5159e42aa161fe0e30175
|
||||
RevenueCat: 3d934653b7e8b09af88fd47e9e84cfaf5d0a89ba
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
|
||||
PODFILE CHECKSUM: ec83c31511fbc978a9918c6fda235238118483f5
|
||||
PODFILE CHECKSUM: 016564c560c4c9dbcb210e12c7aa6039072645f1
|
||||
|
||||
COCOAPODS: 1.14.2
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -57,7 +57,7 @@ void main() async {
|
||||
/// 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.isIOS) {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
|
||||
await JustAudioBackground.init(
|
||||
androidNotificationChannelId: 'com.ryanheise.bg_demo.channel.audio',
|
||||
androidNotificationChannelName: 'Audio playback',
|
||||
|
||||
@@ -8,8 +8,10 @@ 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:
|
||||
/// - [fourchan]
|
||||
/// - [github]
|
||||
/// - [googlenews]
|
||||
/// - [lemmy]
|
||||
/// - [mastodon]
|
||||
/// - [medium]
|
||||
/// - [nitter]
|
||||
@@ -19,7 +21,6 @@ import 'package:feeddeck/utils/fd_icons.dart';
|
||||
/// - [rss]
|
||||
/// - [stackoverflow]
|
||||
/// - [tumblr]
|
||||
/// - [x]
|
||||
/// - [youtube]
|
||||
///
|
||||
/// The [none] value is not valid and just here as a fallback in case sth. odd
|
||||
@@ -27,8 +28,10 @@ import 'package:feeddeck/utils/fd_icons.dart';
|
||||
/// the list, so that we can loop though the types in a ListView / GridView
|
||||
/// builder via `FDSourceType.values.length - 1`.
|
||||
enum FDSourceType {
|
||||
fourchan,
|
||||
github,
|
||||
googlenews,
|
||||
lemmy,
|
||||
mastodon,
|
||||
medium,
|
||||
nitter,
|
||||
@@ -38,7 +41,6 @@ enum FDSourceType {
|
||||
rss,
|
||||
stackoverflow,
|
||||
tumblr,
|
||||
// x,
|
||||
youtube,
|
||||
none,
|
||||
}
|
||||
@@ -55,10 +57,14 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
/// [toLocalizedString] returns a localized string for a source type.
|
||||
String toLocalizedString() {
|
||||
switch (this) {
|
||||
case FDSourceType.fourchan:
|
||||
return '4chan';
|
||||
case FDSourceType.github:
|
||||
return 'GitHub';
|
||||
case FDSourceType.googlenews:
|
||||
return 'Google News';
|
||||
case FDSourceType.lemmy:
|
||||
return 'Lemmy';
|
||||
case FDSourceType.mastodon:
|
||||
return 'Mastodon';
|
||||
case FDSourceType.medium:
|
||||
@@ -77,8 +83,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return 'StackOverflow';
|
||||
case FDSourceType.tumblr:
|
||||
return 'Tumblr';
|
||||
// case FDSourceType.x:
|
||||
// return 'X';
|
||||
case FDSourceType.youtube:
|
||||
return 'YouTube';
|
||||
default:
|
||||
@@ -89,10 +93,14 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
/// [icon] returns the icon for a source.
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case FDSourceType.fourchan:
|
||||
return FDIcons.fourchan;
|
||||
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:
|
||||
@@ -111,8 +119,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return FDIcons.stackoverflow;
|
||||
case FDSourceType.tumblr:
|
||||
return FDIcons.tumblr;
|
||||
// case FDSourceType.x:
|
||||
// return FDIcons.x;
|
||||
case FDSourceType.youtube:
|
||||
return FDIcons.youtube;
|
||||
default:
|
||||
@@ -123,10 +129,14 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
/// [bgColor] returns the background color for the source icon.
|
||||
Color get bgColor {
|
||||
switch (this) {
|
||||
case FDSourceType.fourchan:
|
||||
return const Color(0xff880000);
|
||||
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:
|
||||
@@ -145,8 +155,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return const Color(0xffef8236);
|
||||
case FDSourceType.tumblr:
|
||||
return const Color(0xff34526f);
|
||||
// case FDSourceType.x:
|
||||
// return const Color(0xff000000);
|
||||
case FDSourceType.youtube:
|
||||
return const Color(0xffff0000);
|
||||
default:
|
||||
@@ -158,10 +166,14 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
/// used toether with the [bgColor].
|
||||
Color get fgColor {
|
||||
switch (this) {
|
||||
case FDSourceType.fourchan:
|
||||
return const Color(0xffffffff);
|
||||
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:
|
||||
@@ -180,8 +192,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
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:
|
||||
@@ -258,8 +268,10 @@ class FDSource {
|
||||
/// [FDSourceOptions] defines all options for the different source types which
|
||||
/// are available.
|
||||
class FDSourceOptions {
|
||||
String? fourchan;
|
||||
FDGitHubOptions? github;
|
||||
FDGoogleNewsOptions? googlenews;
|
||||
String? lemmy;
|
||||
String? mastodon;
|
||||
String? medium;
|
||||
String? nitter;
|
||||
@@ -269,12 +281,13 @@ class FDSourceOptions {
|
||||
String? rss;
|
||||
FDStackOverflowOptions? stackoverflow;
|
||||
String? tumblr;
|
||||
String? x;
|
||||
String? youtube;
|
||||
|
||||
FDSourceOptions({
|
||||
this.fourchan,
|
||||
this.github,
|
||||
this.googlenews,
|
||||
this.lemmy,
|
||||
this.mastodon,
|
||||
this.medium,
|
||||
this.nitter,
|
||||
@@ -284,12 +297,15 @@ class FDSourceOptions {
|
||||
this.rss,
|
||||
this.stackoverflow,
|
||||
this.tumblr,
|
||||
this.x,
|
||||
this.youtube,
|
||||
});
|
||||
|
||||
factory FDSourceOptions.fromJson(Map<String, dynamic> responseData) {
|
||||
return FDSourceOptions(
|
||||
fourchan: responseData.containsKey('fourchan') &&
|
||||
responseData['fourchan'] != null
|
||||
? responseData['fourchan']
|
||||
: null,
|
||||
github:
|
||||
responseData.containsKey('github') && responseData['github'] != null
|
||||
? FDGitHubOptions.fromJson(responseData['github'])
|
||||
@@ -298,6 +314,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']
|
||||
@@ -333,9 +352,6 @@ class FDSourceOptions {
|
||||
responseData.containsKey('tumblr') && responseData['tumblr'] != null
|
||||
? responseData['tumblr']
|
||||
: null,
|
||||
x: responseData.containsKey('x') && responseData['x'] != null
|
||||
? responseData['x']
|
||||
: null,
|
||||
youtube:
|
||||
responseData.containsKey('youtube') && responseData['youtube'] != null
|
||||
? responseData['youtube']
|
||||
@@ -345,8 +361,10 @@ class FDSourceOptions {
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'fourchan': fourchan,
|
||||
'github': github?.toJson(),
|
||||
'googlenews': googlenews?.toJson(),
|
||||
'lemmy': lemmy,
|
||||
'mastodon': mastodon,
|
||||
'medium': medium,
|
||||
'nitter': nitter,
|
||||
@@ -356,7 +374,6 @@ class FDSourceOptions {
|
||||
'rss': rss,
|
||||
'stackoverflow': stackoverflow?.toJson(),
|
||||
'tumblr': tumblr,
|
||||
'x': x,
|
||||
'youtube': youtube,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Flutter icons [FDIcons]
|
||||
/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com
|
||||
/// Flutter icons FDIcons
|
||||
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
|
||||
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
|
||||
///
|
||||
/// To use this font, place it in your fonts/ directory and include the
|
||||
@@ -63,4 +63,8 @@ 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);
|
||||
static const IconData fourchan =
|
||||
IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -149,6 +149,7 @@ 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(
|
||||
@@ -175,6 +176,7 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
dividerHeight: 0,
|
||||
onTap: (int index) {
|
||||
/// When the user clicks on a tab we update the index in
|
||||
/// the [LayoutRepository] so that we can use it as
|
||||
|
||||
@@ -39,7 +39,7 @@ class _HomeState extends State<Home> {
|
||||
});
|
||||
|
||||
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||
_appLinks.allUriLinkStream.listen((uri) {
|
||||
_appLinks.uriLinkStream.listen((uri) {
|
||||
if (uri
|
||||
.toString()
|
||||
.startsWith('app.feeddeck.feeddeck://signin-callback/')) {
|
||||
|
||||
@@ -7,6 +7,8 @@ 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_fourchan.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';
|
||||
@@ -60,6 +62,12 @@ class ItemDetails extends StatelessWidget {
|
||||
|
||||
Widget _buildDetails() {
|
||||
switch (source.type) {
|
||||
case FDSourceType.fourchan:
|
||||
return ItemDetailsFourChan(
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
|
||||
/// Sources with type [FDSourceType.github] do not provide a details view,
|
||||
/// because we directly open the link, when the user clicks on the
|
||||
/// corresponding preview item.
|
||||
@@ -71,6 +79,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,
|
||||
@@ -116,11 +129,6 @@ class ItemDetails extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
// case FDSourceType.x:
|
||||
// return ItemDetailsX(
|
||||
// item: item,
|
||||
// source: source,
|
||||
// );
|
||||
case FDSourceType.youtube:
|
||||
return ItemDetailsYoutube(
|
||||
item: item,
|
||||
@@ -202,7 +210,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),
|
||||
),
|
||||
|
||||
@@ -2,13 +2,12 @@ import 'package:flutter/material.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_gallery.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
|
||||
class ItemDetailsX extends StatelessWidget {
|
||||
const ItemDetailsX({
|
||||
class ItemDetailsFourChan extends StatelessWidget {
|
||||
const ItemDetailsFourChan({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.source,
|
||||
@@ -23,6 +22,9 @@ class ItemDetailsX extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ItemTitle(
|
||||
itemTitle: item.title,
|
||||
),
|
||||
ItemSubtitle(
|
||||
item: item,
|
||||
source: source,
|
||||
@@ -32,16 +34,6 @@ class ItemDetailsX extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
90
app/lib/widgets/item/details/item_details_lemmy.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
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_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';
|
||||
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!,
|
||||
);
|
||||
}
|
||||
|
||||
if (item.media!.startsWith('https://piped.video/watch?v=') ||
|
||||
item.media!.startsWith('https://piped.video/')) {
|
||||
return ItemPipedVideo(
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ 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_videos.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
|
||||
|
||||
class ItemDetailsMastodon extends StatelessWidget {
|
||||
const ItemDetailsMastodon({
|
||||
@@ -18,6 +20,116 @@ 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;
|
||||
}
|
||||
|
||||
/// [_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 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,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
final pipedUrl =
|
||||
item.description != null ? _getPipedUrl(item.description!) : null;
|
||||
|
||||
if (pipedUrl != null) {
|
||||
return [
|
||||
ItemPipedVideo(
|
||||
item.media,
|
||||
pipedUrl,
|
||||
),
|
||||
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(
|
||||
@@ -28,32 +140,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'] != 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,
|
||||
),
|
||||
..._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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ 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_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';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
|
||||
|
||||
class ItemDetailsReddit extends StatelessWidget {
|
||||
const ItemDetailsReddit({
|
||||
@@ -16,6 +18,98 @@ 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;
|
||||
}
|
||||
|
||||
/// [_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 YouTube link, we render the [ItemYoutubeVideo]
|
||||
/// and the [ItemDescription] widgets. If the description contains a Piped
|
||||
/// link, we render the [ItemPipedVideo] and the [ItemDescription] widget. If
|
||||
/// the description does not contain a YouTube or Piped 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,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
final pipedUrl =
|
||||
item.description != null ? _getPipedUrl(item.description!) : null;
|
||||
|
||||
if (pipedUrl != null) {
|
||||
return [
|
||||
ItemPipedVideo(
|
||||
item.media,
|
||||
pipedUrl,
|
||||
),
|
||||
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 +123,7 @@ class ItemDetailsReddit extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
..._buildDescription(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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({
|
||||
@@ -19,10 +20,23 @@ class ItemDetailsRSS extends StatelessWidget {
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
/// [_buildImage] renders the [item.media] when the [shouldBeRendered] is
|
||||
/// `true`. If it is `false` an empty container is returned.
|
||||
Widget _buildImage(bool shouldBeRendered) {
|
||||
if (!shouldBeRendered) {
|
||||
/// [_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();
|
||||
}
|
||||
|
||||
@@ -33,12 +47,6 @@ class ItemDetailsRSS extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
/// 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.
|
||||
final descriptionContainImage =
|
||||
parse(item.description).querySelectorAll('img').isNotEmpty;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -50,7 +58,7 @@ class ItemDetailsRSS extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
_buildImage(!descriptionContainImage),
|
||||
_buildMedia(),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,15 @@ class ItemDescription extends StatelessWidget {
|
||||
codeblockDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
),
|
||||
blockquoteDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Constants.primary,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapLink: (text, href, title) {
|
||||
if (href != null) {
|
||||
@@ -158,7 +167,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+'), ' '),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -25,22 +25,12 @@ class ItemVideos extends StatelessWidget {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
separatorBuilder: (context, index) {
|
||||
return const SizedBox(
|
||||
height: Constants.spacingMiddle,
|
||||
);
|
||||
},
|
||||
itemCount: videos!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ItemVideoPlayer(video: videos![index]);
|
||||
},
|
||||
),
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: videos!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ItemVideoPlayer(video: videos![index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +51,9 @@ class ItemVideoQuality {
|
||||
/// 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.
|
||||
@@ -68,10 +61,12 @@ 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
|
||||
@@ -99,43 +94,60 @@ class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
|
||||
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),
|
||||
return SafeArea(
|
||||
child: 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(),
|
||||
),
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
separatorBuilder: (context, index) {
|
||||
return const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
);
|
||||
},
|
||||
itemCount: widget.qualities!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
player.open(
|
||||
Media(widget.qualities![index].video),
|
||||
play: true,
|
||||
);
|
||||
},
|
||||
title: Text(widget.qualities![index].quality),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -194,13 +206,32 @@ class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
|
||||
];
|
||||
}
|
||||
|
||||
/// [_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();
|
||||
player.open(
|
||||
Media(widget.video),
|
||||
play: false,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_playerOpen(widget.video);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -211,46 +242,51 @@ class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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),
|
||||
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 MaterialVideoControlsThemeData(
|
||||
fullscreen: const MaterialDesktopVideoControlsThemeData(
|
||||
seekBarPositionColor: Constants.primary,
|
||||
seekBarThumbColor: Constants.primary,
|
||||
),
|
||||
child: Video(
|
||||
controller: controller,
|
||||
controls: kIsWeb ||
|
||||
Platform.isLinux ||
|
||||
Platform.isMacOS ||
|
||||
Platform.isWindows
|
||||
? MaterialDesktopVideoControls
|
||||
: MaterialVideoControls,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
import 'package:feeddeck/utils/constants.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';
|
||||
@@ -66,18 +65,14 @@ class _ItemYoutubeVideoNativeState extends State<ItemYoutubeVideoNative> {
|
||||
if (snapshot.connectionState == ConnectionState.none ||
|
||||
snapshot.connectionState == ConnectionState.waiting ||
|
||||
snapshot.hasError ||
|
||||
snapshot.data == null) {
|
||||
snapshot.data == null ||
|
||||
snapshot.data!.isEmpty) {
|
||||
return ItemMedia(itemMedia: widget.imageUrl);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
child: ItemVideoPlayer(
|
||||
video: snapshot.data!.first.video,
|
||||
qualities: snapshot.data,
|
||||
),
|
||||
return ItemVideoPlayer(
|
||||
video: snapshot.data!.first.video,
|
||||
qualities: snapshot.data,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,6 +7,33 @@ 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,
|
||||
@@ -27,11 +54,8 @@ class _ItemYoutubeVideoWebState extends State<ItemYoutubeVideoWeb> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_iframeElement.src =
|
||||
'https://www.youtube-nocookie.com/embed/${widget.videoUrl.replaceFirst(
|
||||
'https://www.youtube.com/watch?v=',
|
||||
'',
|
||||
)}';
|
||||
|
||||
_iframeElement.src = _convertVideoUrl(widget.videoUrl);
|
||||
_iframeElement.style.border = 'none';
|
||||
_iframeElement.allowFullscreen = true;
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import 'package:provider/provider.dart';
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/repositories/items_repository.dart';
|
||||
import 'package:feeddeck/widgets/item/preview/item_preview_fourchan.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';
|
||||
@@ -43,6 +45,11 @@ class ItemPreview extends StatelessWidget {
|
||||
/// Based on the [source.type] we display a different preview. The preview
|
||||
/// for each source type is implemented in a separate widget.
|
||||
switch (source.type) {
|
||||
case FDSourceType.fourchan:
|
||||
return ItemPreviewFourChan(
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
case FDSourceType.github:
|
||||
return ItemPreviewGithub(
|
||||
item: item,
|
||||
@@ -53,6 +60,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,
|
||||
@@ -98,11 +110,6 @@ class ItemPreview extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
// case FDSourceType.x:
|
||||
// return ItemPreviewX(
|
||||
// item: item,
|
||||
// source: source,
|
||||
// );
|
||||
case FDSourceType.youtube:
|
||||
return ItemPreviewYoutube(
|
||||
item: item,
|
||||
|
||||
@@ -5,11 +5,12 @@ 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_gallery.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 ItemPreviewX extends StatelessWidget {
|
||||
const ItemPreviewX({
|
||||
class ItemPreviewFourChan extends StatelessWidget {
|
||||
const ItemPreviewFourChan({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.source,
|
||||
@@ -25,24 +26,23 @@ class ItemPreviewX extends StatelessWidget {
|
||||
onTap: () => showDetails(context, item, source),
|
||||
children: [
|
||||
ItemSource(
|
||||
sourceTitle: item.author ?? '',
|
||||
sourceSubtitle: '${source.type.toLocalizedString()}: ${source.title}',
|
||||
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.markdown,
|
||||
),
|
||||
ItemMediaGallery(
|
||||
itemMedias: item.options != null && item.options!.containsKey('media')
|
||||
? (item.options!['media'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
tagetFormat: DescriptionFormat.plain,
|
||||
),
|
||||
],
|
||||
);
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,10 +79,10 @@ 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 {
|
||||
void _showActionsMenuLarge() async {
|
||||
HapticFeedback.heavyImpact();
|
||||
|
||||
final RenderObject? overlay =
|
||||
@@ -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();
|
||||
|
||||
@@ -157,56 +185,72 @@ class _ItemActionsState extends State<ItemActions> {
|
||||
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),
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(
|
||||
Constants.spacingMiddle,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
_read(mainContext);
|
||||
},
|
||||
leading: widget.item.isRead
|
||||
? const Icon(Icons.visibility_off)
|
||||
: const Icon(Icons.visibility),
|
||||
title: widget.item.isRead
|
||||
? const Text('Mark as Unread')
|
||||
: const Text('Mark as Read'),
|
||||
padding: const EdgeInsets.only(
|
||||
left: Constants.spacingMiddle,
|
||||
right: Constants.spacingMiddle,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.background,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
_bookmark(mainContext);
|
||||
},
|
||||
leading: widget.item.isBookmarked
|
||||
? const Icon(Icons.bookmark)
|
||||
: const Icon(Icons.bookmark_outline),
|
||||
title: widget.item.isBookmarked
|
||||
? const Text('Remove Bookmark')
|
||||
: const Text('Add Bookmark'),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
_read(mainContext);
|
||||
},
|
||||
leading: widget.item.isRead
|
||||
? const Icon(Icons.visibility_off)
|
||||
: const Icon(Icons.visibility),
|
||||
title: widget.item.isRead
|
||||
? const Text('Mark as Unread')
|
||||
: const Text('Mark as Read'),
|
||||
),
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
_bookmark(mainContext);
|
||||
},
|
||||
leading: widget.item.isBookmarked
|
||||
? const Icon(Icons.bookmark)
|
||||
: const Icon(Icons.bookmark_outline),
|
||||
title: widget.item.isBookmarked
|
||||
? 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 +370,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),
|
||||
onLongPress: () => _showActionsMenuLarge(),
|
||||
onSecondaryTapDown:
|
||||
kIsWeb ? null : (details) => _getTapPositionLarge(details),
|
||||
onSecondaryTap: kIsWeb ? null : () => _showActionsMenuLarge(),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
|
||||
@@ -55,6 +55,15 @@ class ItemDescription extends StatelessWidget {
|
||||
codeblockDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
),
|
||||
blockquoteDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Constants.primary,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapLink: (text, href, title) {
|
||||
if (href != null) {
|
||||
@@ -81,6 +90,10 @@ 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();
|
||||
@@ -91,7 +104,7 @@ class ItemDescription extends StatelessWidget {
|
||||
bottom: Constants.spacingExtraSmall,
|
||||
),
|
||||
child: Text(
|
||||
content.trim(),
|
||||
content.trim().split('\n').where((line) => line != '').join('\n'),
|
||||
maxLines: 5,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -121,7 +134,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+'), ' '),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,60 +18,62 @@ class SettingsAccountsActions extends StatelessWidget {
|
||||
|
||||
@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),
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(
|
||||
Constants.spacingMiddle,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
reconnect();
|
||||
},
|
||||
leading: const Icon(
|
||||
Icons.link,
|
||||
),
|
||||
title: const Text(
|
||||
'Re-Connect',
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
left: Constants.spacingMiddle,
|
||||
right: Constants.spacingMiddle,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.background,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
delete();
|
||||
},
|
||||
leading: const Icon(
|
||||
Icons.delete,
|
||||
color: Constants.error,
|
||||
),
|
||||
title: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(
|
||||
color: Constants.error,
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
reconnect();
|
||||
},
|
||||
leading: const Icon(
|
||||
Icons.link,
|
||||
),
|
||||
title: const Text(
|
||||
'Re-Connect',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
delete();
|
||||
},
|
||||
leading: const Icon(
|
||||
Icons.delete,
|
||||
color: Constants.error,
|
||||
),
|
||||
title: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(
|
||||
color: Constants.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -152,60 +152,62 @@ class SettingsProfileSignOutActions extends StatelessWidget {
|
||||
|
||||
@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),
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(
|
||||
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',
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
left: Constants.spacingMiddle,
|
||||
right: Constants.spacingMiddle,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.background,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:feeddeck/models/column.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/source/add/add_source_fourchan.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';
|
||||
@@ -37,6 +41,9 @@ class AddSource extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AddSourceState extends State<AddSource> {
|
||||
final List<FDSourceType> _sourceTypeValues = FDSourceType.values
|
||||
.whereNot((e) => e == FDSourceType.nitter || e == FDSourceType.none)
|
||||
.toList();
|
||||
FDSourceType _sourceType = FDSourceType.none;
|
||||
|
||||
/// [_buildBody] returns a list of all supported source types when no source
|
||||
@@ -44,6 +51,10 @@ class _AddSourceState extends State<AddSource> {
|
||||
/// user selected a source type, the functions returns the form for the
|
||||
/// selected source type.
|
||||
Widget _buildBody() {
|
||||
if (_sourceType == FDSourceType.fourchan) {
|
||||
return AddSourceFourChan(column: widget.column);
|
||||
}
|
||||
|
||||
if (_sourceType == FDSourceType.github) {
|
||||
return AddSourceGitHub(column: widget.column);
|
||||
}
|
||||
@@ -52,6 +63,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);
|
||||
}
|
||||
@@ -88,10 +103,6 @@ class _AddSourceState extends State<AddSource> {
|
||||
return AddSourceTumblr(column: widget.column);
|
||||
}
|
||||
|
||||
// if (_sourceType == FDSourceType.x) {
|
||||
// return AddSourceX(column: widget.column);
|
||||
// }
|
||||
|
||||
if (_sourceType == FDSourceType.youtube) {
|
||||
return AddSourceYouTube(column: widget.column);
|
||||
}
|
||||
@@ -103,14 +114,14 @@ class _AddSourceState extends State<AddSource> {
|
||||
height: Constants.spacingMiddle,
|
||||
);
|
||||
},
|
||||
itemCount: FDSourceType.values.length - 1,
|
||||
itemCount: _sourceTypeValues.length,
|
||||
itemBuilder: (context, index) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_sourceType = FDSourceType.values[index];
|
||||
_sourceType = _sourceTypeValues[index];
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
@@ -120,7 +131,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].bgColor,
|
||||
color: _sourceTypeValues[index].bgColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
@@ -128,7 +139,7 @@ class _AddSourceState extends State<AddSource> {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SourceIcon(
|
||||
type: FDSourceType.values[index],
|
||||
type: _sourceTypeValues[index],
|
||||
icon: null,
|
||||
size: 48,
|
||||
),
|
||||
@@ -136,7 +147,7 @@ class _AddSourceState extends State<AddSource> {
|
||||
height: Constants.spacingSmall,
|
||||
),
|
||||
Text(
|
||||
FDSourceType.values[index].toLocalizedString(),
|
||||
_sourceTypeValues[index].toLocalizedString(),
|
||||
style: TextStyle(
|
||||
/// Since we are using the brand color as background
|
||||
/// color, we are using the same color as for the icon
|
||||
@@ -144,7 +155,7 @@ class _AddSourceState extends State<AddSource> {
|
||||
/// to use a generic color as background the following
|
||||
/// line can be used:
|
||||
/// color: Constants.onSecondary,
|
||||
color: FDSourceType.values[index].fgColor,
|
||||
color: _sourceTypeValues[index].fgColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
440
app/lib/widgets/source/add/add_source_fourchan.dart
Normal file
@@ -0,0 +1,440 @@
|
||||
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 4chan source can be used to follow your favorite 4chan boards.
|
||||
''';
|
||||
|
||||
/// The [AddSourceFourChan] widget is used to display the form to add a new
|
||||
/// 4chan board.
|
||||
class AddSourceFourChan extends StatefulWidget {
|
||||
const AddSourceFourChan({
|
||||
super.key,
|
||||
required this.column,
|
||||
});
|
||||
|
||||
final FDColumn column;
|
||||
|
||||
@override
|
||||
State<AddSourceFourChan> createState() => _AddSourceFourChanState();
|
||||
}
|
||||
|
||||
class _AddSourceFourChanState extends State<AddSourceFourChan> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
String _fourChanBoard = 'a';
|
||||
bool _isLoading = false;
|
||||
String _error = '';
|
||||
|
||||
/// [_addSource] adds a new 4chan board. The user can select a board from the
|
||||
/// dropdown and we will generate the corresponding 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.fourchan,
|
||||
FDSourceOptions(
|
||||
fourchan: _fourChanBoard,
|
||||
),
|
||||
);
|
||||
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
|
||||
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,
|
||||
),
|
||||
DropdownButton<String>(
|
||||
value: _fourChanBoard,
|
||||
isExpanded: true,
|
||||
underline: Container(height: 1, color: Constants.primary),
|
||||
onChanged: (String? value) {
|
||||
setState(() {
|
||||
_fourChanBoard = value!;
|
||||
});
|
||||
},
|
||||
items: boards.map((FourChanBoard value) {
|
||||
return DropdownMenuItem(
|
||||
value: value.id,
|
||||
child: Text(value.name),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [FourChanBoard] is the model for a supported 4chan boards.
|
||||
class FourChanBoard {
|
||||
String id;
|
||||
String name;
|
||||
|
||||
FourChanBoard({
|
||||
required this.id,
|
||||
required this.name,
|
||||
});
|
||||
}
|
||||
|
||||
/// [boards] is the list of all supported 4chan boards.
|
||||
final boards = <FourChanBoard>[
|
||||
FourChanBoard(
|
||||
id: 'a',
|
||||
name: 'Anime & Manga',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'c',
|
||||
name: 'Anime/Cute',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'w',
|
||||
name: 'Anime/Wallpapers',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'm',
|
||||
name: 'Mecha',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'cgl',
|
||||
name: 'Cosplay & EGL',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'cm',
|
||||
name: 'Cute/Male',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'f',
|
||||
name: 'Flash',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'n',
|
||||
name: 'Transportation',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'jp',
|
||||
name: 'Otaku Culture',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vt',
|
||||
name: 'Virtual YouTubers',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'v',
|
||||
name: 'Video Games',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vg',
|
||||
name: 'Video Game Generals',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vm',
|
||||
name: 'Video Games/Multiplayer',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vmg',
|
||||
name: 'Video Games/Mobile',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vp',
|
||||
name: 'Pokémon',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vr',
|
||||
name: 'Retro Games',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vrpg',
|
||||
name: 'Video Games/RPG',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vst',
|
||||
name: 'Video Games/Strategy',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'co',
|
||||
name: 'Comics & Cartoons',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'g',
|
||||
name: 'Technology',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'tv',
|
||||
name: 'Television & Film',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'k',
|
||||
name: 'Weapons',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'o',
|
||||
name: 'Auto',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'an',
|
||||
name: 'Animals & Nature',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'tg',
|
||||
name: 'Traditional Games',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'sp',
|
||||
name: 'Sports',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'xs',
|
||||
name: 'Extreme Sports',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'pw',
|
||||
name: 'Professional Wrestling',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'sci',
|
||||
name: 'Science & Math',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'his',
|
||||
name: 'History & Humanities',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'int',
|
||||
name: 'International',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'out',
|
||||
name: 'Outdoors',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'toy',
|
||||
name: 'Toys',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'i',
|
||||
name: 'Oekaki',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'po',
|
||||
name: 'Papercraft & Origami',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'p',
|
||||
name: 'Photography',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'ck',
|
||||
name: 'Food & Cooking',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'ic',
|
||||
name: 'Artwork/Critique',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'wg',
|
||||
name: 'Wallpapers/General',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'lit',
|
||||
name: 'Literature',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'mu',
|
||||
name: 'Music',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'fa',
|
||||
name: 'Fashion',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: '3',
|
||||
name: '3DCG',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'gd',
|
||||
name: 'Graphic Design',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'diy',
|
||||
name: 'Do-It-Yourself',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'wsg',
|
||||
name: 'Worksafe GIF',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'qst',
|
||||
name: 'Quests',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'biz',
|
||||
name: 'Business & Finance',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'trv',
|
||||
name: 'Travel',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'fit',
|
||||
name: 'Fitness',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'x',
|
||||
name: 'Paranormal',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'adv',
|
||||
name: 'Advice',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'lgbt',
|
||||
name: 'LGBT',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'mlp',
|
||||
name: 'Pony',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'news',
|
||||
name: 'Current News',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'wsr',
|
||||
name: 'Worksafe Requests',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'vip',
|
||||
name: 'Very Important Posts',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'b',
|
||||
name: 'Random (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'r9k',
|
||||
name: 'ROBOT9001 (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'pol',
|
||||
name: 'Politically Incorrect (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'bant',
|
||||
name: 'International/Random (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'soc',
|
||||
name: 'Cams & Meetups (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 's4s',
|
||||
name: 'Shit 4chan Says (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 's',
|
||||
name: 'Sexy Beautiful Women (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'hc',
|
||||
name: 'Hardcore (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'hm',
|
||||
name: 'Handsome Men (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'h',
|
||||
name: 'Hentai (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'e',
|
||||
name: 'Ecchi (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'u',
|
||||
name: 'Yuri (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'd',
|
||||
name: 'Hentai/Alternative (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'y',
|
||||
name: 'Yaoi (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 't',
|
||||
name: 'Torrents (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'hr',
|
||||
name: 'High Resolution (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'gif',
|
||||
name: 'Adult GIF (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'aco',
|
||||
name: 'Adult Cartoons (NSFW)',
|
||||
),
|
||||
FourChanBoard(
|
||||
id: 'r',
|
||||
name: 'Adult Requests (NSFW)',
|
||||
),
|
||||
];
|
||||
@@ -1,21 +1,31 @@
|
||||
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 X (formerly Twitter) source can be used to follow a user on X, e.g. `@rico_berger`.
|
||||
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 [AddSourceX] widget is used to display the form to add a new X
|
||||
/// (formerly) source.
|
||||
class AddSourceX extends StatefulWidget {
|
||||
const AddSourceX({
|
||||
/// The [AddSourceLemmy] widget is used to display the form to add a new Lemmy
|
||||
/// source.
|
||||
class AddSourceLemmy extends StatefulWidget {
|
||||
const AddSourceLemmy({
|
||||
super.key,
|
||||
required this.column,
|
||||
});
|
||||
@@ -23,17 +33,17 @@ class AddSourceX extends StatefulWidget {
|
||||
final FDColumn column;
|
||||
|
||||
@override
|
||||
State<AddSourceX> createState() => _AddSourceXState();
|
||||
State<AddSourceLemmy> createState() => _AddSourceLemmyState();
|
||||
}
|
||||
|
||||
class _AddSourceXState extends State<AddSourceX> {
|
||||
class _AddSourceLemmyState extends State<AddSourceLemmy> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _xController = TextEditingController();
|
||||
final _lemmyController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
String _error = '';
|
||||
|
||||
/// [_addSource] adds a new X source where the user can provide the username
|
||||
/// of a x user.
|
||||
/// [_addSource] adds a new Lemmy source. The user can provide a Lemmy url,
|
||||
/// which could be be a community or user or the corresponding RSS feed.
|
||||
Future<void> _addSource() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
@@ -41,14 +51,14 @@ class _AddSourceXState extends State<AddSourceX> {
|
||||
});
|
||||
|
||||
try {
|
||||
// AppRepository app = Provider.of<AppRepository>(context, listen: false);
|
||||
// await app.addSource(
|
||||
// widget.column.id,
|
||||
// FDSourceType.x,
|
||||
// FDSourceOptions(
|
||||
// x: _xController.text,
|
||||
// ),
|
||||
// );
|
||||
AppRepository app = Provider.of<AppRepository>(context, listen: false);
|
||||
await app.addSource(
|
||||
widget.column.id,
|
||||
FDSourceType.lemmy,
|
||||
FDSourceOptions(
|
||||
lemmy: _lemmyController.text,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_error = '';
|
||||
@@ -71,7 +81,7 @@ class _AddSourceXState extends State<AddSourceX> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_xController.dispose();
|
||||
_lemmyController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -101,14 +111,14 @@ class _AddSourceXState extends State<AddSourceX> {
|
||||
height: Constants.spacingMiddle,
|
||||
),
|
||||
TextFormField(
|
||||
controller: _xController,
|
||||
controller: _lemmyController,
|
||||
keyboardType: TextInputType.text,
|
||||
autocorrect: false,
|
||||
enableSuggestions: true,
|
||||
maxLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Username',
|
||||
labelText: 'Lemmy 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;
|
||||
|
||||
@@ -3,7 +3,6 @@ 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:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'package:feeddeck/repositories/settings_repository.dart';
|
||||
|
||||
@@ -14,7 +13,7 @@ import 'package:feeddeck/repositories/settings_repository.dart';
|
||||
String getImageUrl(String imageUrl) {
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
if (kIsWeb) {
|
||||
return '${Supabase.instance.client.functionsUrl}/image-proxy-v1?media=${Uri.encodeQueryComponent(imageUrl)}';
|
||||
return '${SettingsRepository().supabaseUrl}/functions/v1/image-proxy-v1?media=${Uri.encodeQueryComponent(imageUrl)}';
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
|
||||
@@ -50,6 +50,141 @@
|
||||
</categories>
|
||||
|
||||
<releases>
|
||||
<release version="v1.4.0" date="2024-08-04">
|
||||
<description>
|
||||
<p>Added</p>
|
||||
<ul>
|
||||
<li>#157: [core] Add Indexes to Improve Query Performance @ricoberger</li>
|
||||
<li>#145: [mastodon] Add Support for Piped Videos @ricoberger</li>
|
||||
<li>#144: [reddit] Add Support for Piped Videos @ricoberger</li>
|
||||
<li>#143: [lemmy] Add Support for Piped Videos @ricoberger</li>
|
||||
<li>#142: [4chan] Add Support for 4chan @ricoberger</li>
|
||||
</ul>
|
||||
|
||||
<p>Fixed</p>
|
||||
<ul>
|
||||
<li>#171: Fix ESLint Dependency Mismatch @ricoberger</li>
|
||||
<li>#166: Revert "Bump the npm group in /landing with 10 updates" @ricoberger</li>
|
||||
<li>#148: [core] Fix Blockquote Style @ricoberger</li>
|
||||
<li>#141: [rss] Fix Image Parsing @ricoberger</li>
|
||||
<li>#140: [core] Remove referer Check in image-proxy-v1 Function @ricoberger</li>
|
||||
</ul>
|
||||
|
||||
<p>Changed</p>
|
||||
<ul>
|
||||
<li>#181: Bump the pub group in /app with 12 updates @dependabot</li>
|
||||
<li>#180: Bump the npm group in /landing with 8 updates @dependabot</li>
|
||||
<li>#178: Bump braces from 3.0.2 to 3.0.3 in /landing @dependabot</li>
|
||||
<li>#174: Bump the npm group in /landing with 8 updates @dependabot</li>
|
||||
<li>#176: Bump the pub group in /app with 6 updates @dependabot</li>
|
||||
<li>#175: Bump docker/build-push-action from 5 to 6 in the github-actions group @dependabot</li>
|
||||
<li>#172: [core] Wrap Actions in SafeArea Widget @ricoberger</li>
|
||||
<li>#167: Bump the pub group across 1 directory with 17 updates @dependabot</li>
|
||||
<li>#168: Bump the npm group in /landing with 10 updates @dependabot</li>
|
||||
<li>#165: Bump the npm group in /landing with 10 updates @dependabot</li>
|
||||
<li>#163: [nitter] Disable Adding of new Sources @ricoberger</li>
|
||||
<li>#162: [x] Remove X Source @ricoberger</li>
|
||||
<li>#161: [core] Update Flutter Version to 3.19.5 @ricoberger</li>
|
||||
<li>#160: Bump the pub group in /app with 9 updates @dependabot</li>
|
||||
<li>#159: Bump the github-actions group with 1 update @dependabot</li>
|
||||
<li>#158: Bump the npm group in /landing with 9 updates @dependabot</li>
|
||||
<li>#151: Bump the pub group in /app with 3 updates @dependabot</li>
|
||||
<li>#156: Bump the npm group in /landing with 2 updates @dependabot</li>
|
||||
<li>#153: Bump the docker group in /supabase/functions/_cmd with 1 update @dependabot</li>
|
||||
<li>#152: Bump the github-actions group with 1 update @dependabot</li>
|
||||
<li>#150: Bump the npm-landing group in /landing with 6 updates @dependabot</li>
|
||||
<li>#149: [core] Upgrade Flutter Version @ricoberger</li>
|
||||
<li>#115: Bump the npm-email-templates group in /supabase/email-templates with 2 updates @dependabot</li>
|
||||
<li>#147: [core] Use Upstream just_audio_media_kit Version @ricoberger</li>
|
||||
<li>#146: [4chan] Improve Options Parsing @ricoberger</li>
|
||||
</ul>
|
||||
</description>
|
||||
|
||||
<url>https://github.com/feeddeck/feeddeck/releases/tag/v1.2.1</url>
|
||||
</release>
|
||||
|
||||
<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>
|
||||
@@ -64,13 +199,13 @@
|
||||
</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>
|
||||
</ul>
|
||||
|
||||
<p>Changed</p>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#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>
|
||||
@@ -13,6 +14,9 @@
|
||||
#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);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
gtk
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
screen_retriever
|
||||
|
||||
@@ -6,9 +6,6 @@ PODS:
|
||||
- audio_session (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- just_audio (0.0.1):
|
||||
- FlutterMacOS
|
||||
- media_kit_libs_macos_video (1.0.4):
|
||||
@@ -22,12 +19,12 @@ PODS:
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- purchases_flutter (6.4.0):
|
||||
- purchases_flutter (6.30.2):
|
||||
- FlutterMacOS
|
||||
- PurchasesHybridCommon (= 8.0.0)
|
||||
- PurchasesHybridCommon (8.0.0):
|
||||
- RevenueCat (= 4.30.5)
|
||||
- RevenueCat (4.30.5)
|
||||
- PurchasesHybridCommon (= 11.1.0)
|
||||
- PurchasesHybridCommon (11.1.0):
|
||||
- RevenueCat (= 4.43.2)
|
||||
- RevenueCat (4.43.2)
|
||||
- screen_brightness_macos (0.1.0):
|
||||
- FlutterMacOS
|
||||
- screen_retriever (0.0.1):
|
||||
@@ -37,9 +34,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- sign_in_with_apple (0.0.1):
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.2):
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- FMDB (>= 2.7.5)
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
@@ -63,14 +60,13 @@ DEPENDENCIES:
|
||||
- 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`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||
- 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:
|
||||
trunk:
|
||||
- FMDB
|
||||
- PurchasesHybridCommon
|
||||
- RevenueCat
|
||||
|
||||
@@ -106,7 +102,7 @@ EXTERNAL SOURCES:
|
||||
sign_in_with_apple:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
wakelock_plus:
|
||||
@@ -115,29 +111,28 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 4481ed4d71f384b0c3ae5016f4633aa73d32ff67
|
||||
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
|
||||
audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9
|
||||
audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489
|
||||
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: dd2e2b2d7fda0e64ee50ad426487dfa6dbf1bdd8
|
||||
PurchasesHybridCommon: 80262c5ffe6621e3cf3812e6103170f6d7fbcb79
|
||||
RevenueCat: c1e33f4e1f1fd239ba461652f02928e220becc31
|
||||
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
purchases_flutter: 3407100959d2aeb636507b2c98970d850dae57ae
|
||||
PurchasesHybridCommon: 4022d5944cb30ec44ba5159e42aa161fe0e30175
|
||||
RevenueCat: 3d934653b7e8b09af88fd47e9e84cfaf5d0a89ba
|
||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sign_in_with_apple: a9e97e744e8edc36aefc2723111f652102a7a727
|
||||
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
|
||||
PODFILE CHECKSUM: 8d40c19d3cbdb380d870685c3a564c989f1efa52
|
||||
|
||||
COCOAPODS: 1.14.2
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
489
app/pubspec.lock
@@ -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.2.0+8
|
||||
version: 1.4.0+11
|
||||
|
||||
environment:
|
||||
sdk: '>=3.1.3 <4.0.0'
|
||||
sdk: '>=3.3.0 <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
|
||||
@@ -35,41 +35,39 @@ dependencies:
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
app_links: ^3.4.3
|
||||
cached_network_image: ^3.2.3
|
||||
app_links: ^6.1.4
|
||||
cached_network_image: ^3.3.1
|
||||
carousel_slider: ^4.2.1
|
||||
collection: ^1.17.0
|
||||
flutter_cache_manager: ^3.3.1
|
||||
flutter_markdown: ^0.6.14
|
||||
flutter_native_splash: ^2.3.5
|
||||
crypto: ^3.0.3
|
||||
flutter_cache_manager: ^3.4.0
|
||||
flutter_markdown: ^0.7.3
|
||||
flutter_native_splash: ^2.4.1
|
||||
html: ^0.15.4
|
||||
html2md: ^1.2.6
|
||||
intl: ^0.18.1
|
||||
just_audio: ^0.9.32
|
||||
just_audio_background: ^0.0.1-beta.10
|
||||
# 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
|
||||
html2md: ^1.3.2
|
||||
http: ^1.2.2
|
||||
intl: ^0.19.0
|
||||
just_audio: ^0.9.39
|
||||
just_audio_background: ^0.0.1-beta.13
|
||||
just_audio_media_kit: ^2.0.5
|
||||
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
|
||||
provider: ^6.0.4
|
||||
purchases_flutter: ^6.2.0
|
||||
package_info_plus: ^8.0.1
|
||||
piped_client: ^0.1.0
|
||||
provider: ^6.1.2
|
||||
purchases_flutter: ^6.30.2
|
||||
rxdart: ^0.27.7
|
||||
scroll_to_index: ^3.0.1
|
||||
shared_preferences: ^2.1.0
|
||||
supabase_flutter: ^1.10.24
|
||||
timeago: ^3.6.0
|
||||
url_launcher: ^6.2.1
|
||||
window_manager: ^0.3.4
|
||||
youtube_explode_dart: ^2.0.2
|
||||
shared_preferences: ^2.3.0
|
||||
sign_in_with_apple: ^6.1.1
|
||||
supabase_flutter: ^2.5.11
|
||||
timeago: ^3.7.0
|
||||
url_launcher: ^6.3.0
|
||||
window_manager: ^0.3.9
|
||||
youtube_explode_dart: ^2.2.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -80,11 +78,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: ^3.0.1
|
||||
flutter_lints: ^4.0.0
|
||||
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
import_sorter: ^4.6.0
|
||||
msix: ^3.16.6
|
||||
msix: ^3.16.7
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
@@ -171,7 +169,7 @@ msix_config:
|
||||
publisher_display_name: Rico Berger
|
||||
identity_name: 26077RicoBerger.FeedDeck
|
||||
publisher: CN=7740451A-C179-450A-B346-7231CA231332
|
||||
msix_version: 1.2.0.0
|
||||
msix_version: 1.4.0.0
|
||||
logo_path: templates/app-icon/windows.png
|
||||
languages: en-us
|
||||
capabilities: internetClient
|
||||
|
||||
@@ -313,6 +313,34 @@
|
||||
"search": [
|
||||
"pinterest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "08d314902e3800cf1b17c4c1226f45d9",
|
||||
"css": "lemmy",
|
||||
"code": 59414,
|
||||
"src": "custom_icons",
|
||||
"selected": true,
|
||||
"svg": {
|
||||
"path": "M123.3 176C118.7 176 114.1 176.3 109.5 176.7 72.9 181 39.9 200.5 19.7 234.5-0.1 267.8-5 304.5 5.1 338.5 15.2 372.5 39.7 403.5 75.9 427.9 76.1 428.1 76.3 428.2 76.5 428.4 107.6 447.1 138.7 459.4 171.9 465.9 171 479.7 170.7 493.7 171.6 508.3 174.2 551.7 189.8 591.7 213.1 627L129.5 661.1C125.2 662.8 121.8 666.2 120 670.5 117.3 676.9 118.7 684.4 123.7 689.4 127 692.7 131.4 694.5 136 694.5 138.2 694.5 140.5 694.1 142.6 693.2L234.7 655.6C251.7 675.8 270.9 694.4 291.8 710.5 292.7 711.1 293.5 711.6 294.3 712.3L240.5 783.5C238.4 786.5 237.2 790 237.2 793.7 237.2 803.2 245.1 811 254.6 811 259.9 811 264.9 808.6 268.2 804.4L323 732C352.9 750.9 384.5 765 415.8 774.7 431.8 804.7 463.2 824.1 500 824.1 537.1 824.1 568.5 803.2 584.3 773.8 615.3 764 646.7 750 676.3 731L731.8 804.4C735.1 808.7 740.2 811.3 745.7 811.3 754.3 811.3 761.6 804.9 762.8 796.3 763.5 791.8 762.2 787.2 759.5 783.5L704.9 711.2C705.2 711 705.7 710.8 706 710.5 726.7 694.6 745.8 676.4 762.7 656.6L852.5 693.2C861.3 696.8 871.5 692.5 875.1 683.7 876 681.6 876.4 679.4 876.4 677.1 876.4 670.1 872.1 663.7 865.6 661.1L784.6 628.1C808.7 592.5 825 552.3 828.4 508.5 829.5 493.9 829.4 479.7 828.7 465.7 861.7 459.2 892.6 447 923.5 428.4 923.7 428.2 923.9 428 924.2 427.9 960.3 403.4 984.8 372.5 994.9 338.5 1005 304.5 1000.1 267.8 980.3 234.5 960.1 200.5 927.1 180.9 890.5 176.7 885.9 176.2 881.3 176 876.7 175.9 844 175.3 809.1 185.5 775.5 204.8 750.8 219.1 728.6 241 711.2 264.8 662.5 236.9 599 221.1 521 219.9 514 219.7 506.8 219.7 499.7 219.9 412.9 221.3 343.3 237.8 290.8 267.5 290.7 267.4 290.6 267.1 290.5 266.9 272.9 242.3 250 219.6 224.5 204.9 191 185.5 156 175.4 123.3 176L123.3 176ZM135.2 206.6C158.4 208.4 184 216.5 209.3 231.1 229.5 242.8 250 262.6 265.4 284 258.2 289.3 251.3 295 244.7 301 206.4 336.5 183.8 382.7 175.2 435.7 146.8 429.9 120.3 419.3 92.5 402.7 61.1 381.3 41.9 355.9 34.2 330 26.5 304 29.7 277 45.7 250 61.4 223.7 84.4 210.2 112.9 207 120.3 206.2 127.8 206 135.2 206.7L135.2 206.6ZM864.8 206.6C872.2 206 879.7 206.1 887.1 207 915.6 210.2 938.6 223.7 954.2 250 970.3 277 973.5 304 965.8 330 958.1 355.9 938.9 381.2 907.5 402.6 880 419.1 853.8 429.6 825.8 435.5 817.7 381.5 795.1 334.1 756.4 297.9 750.1 292.1 743.6 286.7 736.7 281.6 752 261.2 771.3 242.3 790.7 231.1 816 216.5 841.6 208.4 864.8 206.6L864.8 206.6ZM500.2 250.2C507 250 513.8 250 520.5 250.2 620.1 251.8 690.7 278.1 735.7 320.1 783.6 364.9 804.1 428.4 798.1 506.2 792.7 577.3 747.3 640.7 687.6 686.4 658.7 708.5 626.7 725.3 594.9 737.6 595 735.7 595.5 733.9 595.5 732 595.6 682.1 556.7 639.7 500 639.7 443.3 639.7 403.2 682 404.5 732.3 404.5 734.4 405.1 736.3 405.2 738.4 372.7 726.2 339.9 709.2 310.3 686.5 251 640.9 206.2 577.7 201.9 506.5 197.2 429.1 217.6 367.3 265.2 323.2 312.9 279.2 389.7 252 500.2 250.2ZM348.5 534.7C323.3 534.7 302.8 555.1 302.8 580.4 302.8 605.7 323.3 626.2 348.5 626.2 373.7 626.2 394.2 605.7 394.2 580.4 394.2 555.1 373.7 534.7 348.5 534.7ZM651.9 535.2C626.9 535.2 606.6 555.4 606.6 580.5 606.6 605.5 626.9 625.8 651.9 625.8 676.9 625.8 697.2 605.5 697.2 580.5 697.2 555.4 676.9 535.2 651.9 535.2ZM500 670.2C542.7 670.2 565.2 696.7 565.1 731.9 565.1 764.6 537.1 793.8 500 793.8 461.8 793.8 435.8 770.4 434.9 731.6 434 696.8 457.3 670.2 500 670.2Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"lemmy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "2efb1796652ef55a1fca1f7a3eda4b86",
|
||||
"css": "fourchan",
|
||||
"code": 59415,
|
||||
"src": "custom_icons",
|
||||
"selected": true,
|
||||
"svg": {
|
||||
"path": "M461.3 367.5C461.3 367.5 408.5 45 214.4 45.7 83.6 46.2 32.5 171.8 127.3 200.1 127.3 200.1 14.9 240.6 14.9 312.5 14.9 384.3 193 460 461.3 367.5L461.3 367.5ZM541.7 614.3C541.7 614.3 579.4 939 773.3 947.3 904 952.9 960.9 829.8 867.6 797.1 867.6 797.1 981.7 761.9 985.1 690.2 988.4 618.4 814 534.5 541.7 614.3L541.7 614.3ZM388.7 549.7C388.7 549.7 74.9 641 99.1 833.6 115.3 963.4 246.1 999 262.8 901.5 262.8 901.5 316.6 1008.2 387.9 999.5 459.2 990.8 512.9 804.9 388.7 549.8L388.7 549.7ZM623 447C623 447 945 390.7 942.2 196.6 940.3 65.9 814.1 16.1 786.9 111.2 786.9 111.2 745.2-0.7 673.3 0 601.4 0.8 527.7 179.7 623 447Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"fourchan"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
app/templates/iconfont/fourchan.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 4096 4096" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="_4chan" serif:id="4chan" transform="matrix(4.26802,0,0,4.26802,-137.229,-137.223)">
|
||||
<path d="M474.83,384.842C474.83,384.842 424.165,75.302 237.905,76.022C112.383,76.502 63.359,197.062 154.333,224.174C154.333,224.174 46.448,263.082 46.448,332.019C46.448,400.996 217.353,473.573 474.828,384.801L474.83,384.842ZM552.045,621.726C552.045,621.726 588.193,933.266 774.293,941.263C899.693,946.661 954.355,828.499 864.743,797.15C864.743,797.15 974.308,763.358 977.508,694.503C980.708,625.605 813.361,545.071 552.046,621.727L552.045,621.726ZM405.212,559.744C405.212,559.744 104.028,647.316 127.222,832.176C142.817,956.698 268.377,990.886 284.372,897.356C284.372,897.356 335.955,999.684 404.412,991.366C472.869,983.048 524.374,804.626 405.212,559.746L405.212,559.744ZM630.06,461.098C630.06,461.098 939.04,407.115 936.364,220.856C934.52,95.376 813.44,47.592 787.29,138.843C787.29,138.843 747.302,31.438 678.284,32.155C609.344,32.877 538.569,204.581 630.06,461.098Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
7
app/templates/iconfont/lemmy.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 4096 4096" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="Lemmy" transform="matrix(170.666,0,0,170.666,-0.0042009,-0.0031286)">
|
||||
<path d="M2.959,4.223C2.849,4.224 2.738,4.231 2.627,4.242C1.749,4.343 0.958,4.812 0.473,5.628C-0.002,6.428 -0.12,7.309 0.123,8.125C0.365,8.941 0.952,9.683 1.821,10.27C1.826,10.274 1.831,10.278 1.837,10.281C2.583,10.731 3.329,11.025 4.125,11.181C4.105,11.513 4.098,11.85 4.119,12.199C4.182,13.242 4.555,14.2 5.115,15.048L3.109,15.867C3.006,15.908 2.924,15.989 2.881,16.091C2.815,16.246 2.85,16.426 2.969,16.546C3.047,16.624 3.153,16.669 3.263,16.669C3.318,16.669 3.372,16.659 3.422,16.638L5.632,15.734C6.04,16.22 6.501,16.666 7.004,17.052C7.024,17.067 7.045,17.079 7.064,17.095L5.773,18.805C5.722,18.876 5.694,18.961 5.694,19.048C5.694,19.276 5.882,19.464 6.11,19.464C6.237,19.464 6.358,19.406 6.437,19.306L7.751,17.567C8.469,18.022 9.229,18.36 9.98,18.592C10.363,19.313 11.118,19.779 12,19.779C12.891,19.779 13.644,19.278 14.024,18.572C14.768,18.337 15.52,17.999 16.232,17.545L17.564,19.306C17.642,19.41 17.765,19.472 17.896,19.472C18.103,19.472 18.279,19.317 18.308,19.112C18.323,19.003 18.294,18.893 18.228,18.805L16.918,17.07C16.926,17.063 16.936,17.059 16.944,17.052C17.441,16.671 17.899,16.234 18.306,15.758L20.461,16.637C20.672,16.723 20.916,16.62 21.002,16.41C21.023,16.359 21.034,16.305 21.034,16.251C21.034,16.082 20.931,15.93 20.775,15.866L18.831,15.074C19.408,14.22 19.801,13.255 19.882,12.204C19.908,11.854 19.906,11.513 19.89,11.178C20.68,11.021 21.422,10.728 22.163,10.281C22.169,10.277 22.174,10.273 22.18,10.269C23.048,9.682 23.636,8.941 23.878,8.124C24.12,7.308 24.003,6.427 23.528,5.627C23.043,4.811 22.251,4.342 21.373,4.241C21.262,4.229 21.152,4.223 21.041,4.222C20.255,4.207 19.418,4.451 18.612,4.916C18.019,5.258 17.487,5.783 17.069,6.355C15.899,5.685 14.376,5.307 12.505,5.277C12.335,5.274 12.164,5.274 11.994,5.277C9.909,5.311 8.24,5.707 6.98,6.421C6.977,6.418 6.975,6.41 6.972,6.406C6.549,5.815 5.999,5.27 5.388,4.917C4.583,4.452 3.745,4.209 2.96,4.223L2.959,4.223ZM3.245,4.959C3.802,5.001 4.415,5.195 5.024,5.547C5.509,5.827 6,6.302 6.37,6.816C6.197,6.943 6.031,7.079 5.873,7.225C4.953,8.077 4.412,9.185 4.205,10.458C3.524,10.318 2.887,10.064 2.221,9.664C1.466,9.151 1.005,8.541 0.821,7.919C0.636,7.295 0.713,6.648 1.098,6C1.473,5.37 2.026,5.046 2.71,4.967C2.888,4.948 3.067,4.945 3.245,4.96L3.245,4.959ZM20.755,4.959C20.933,4.945 21.112,4.947 21.29,4.967C21.974,5.045 22.527,5.369 22.902,5.999C23.287,6.647 23.364,7.295 23.179,7.919C22.995,8.541 22.534,9.15 21.779,9.663C21.121,10.059 20.491,10.31 19.819,10.452C19.625,9.155 19.082,8.018 18.153,7.15C18.003,7.011 17.846,6.88 17.682,6.758C18.047,6.268 18.511,5.815 18.976,5.546C19.585,5.195 20.198,5.001 20.755,4.959L20.755,4.959ZM12.006,6.004C12.168,6.001 12.331,6.001 12.493,6.004C14.883,6.043 16.578,6.674 17.656,7.682C18.806,8.757 19.298,10.282 19.155,12.149C19.024,13.856 17.935,15.377 16.503,16.473C15.809,17.004 15.041,17.408 14.278,17.702C14.281,17.657 14.292,17.613 14.292,17.567C14.295,16.371 13.36,15.354 12,15.354C10.639,15.354 9.678,16.369 9.708,17.575C9.709,17.626 9.722,17.672 9.726,17.722C8.945,17.429 8.157,17.021 7.448,16.477C6.025,15.382 4.949,13.864 4.845,12.155C4.732,10.298 5.223,8.816 6.366,7.758C7.509,6.7 9.352,6.047 12.006,6.004ZM8.364,12.833C7.759,12.833 7.268,13.323 7.268,13.929C7.268,14.536 7.759,15.028 8.364,15.028C8.969,15.028 9.461,14.536 9.461,13.929C9.461,13.323 8.969,12.833 8.364,12.833ZM15.646,12.844C15.046,12.844 14.559,13.33 14.559,13.931C14.559,14.531 15.046,15.019 15.646,15.019C16.246,15.019 16.733,14.531 16.733,13.931C16.733,13.33 16.246,12.844 15.646,12.844ZM12,16.084C13.024,16.084 13.565,16.722 13.563,17.566C13.562,18.351 12.891,19.051 12,19.051C11.083,19.051 10.46,18.489 10.437,17.558C10.415,16.724 10.976,16.084 12,16.084Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
29
app/test/widgets/item/preview/utils/item_title_test.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:feeddeck/widgets/item/preview/utils/item_title.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Should render title', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: ItemTitle(itemTitle: 'My Title'),
|
||||
),
|
||||
);
|
||||
expect(find.byType(Container), findsOneWidget);
|
||||
expect(find.byType(Text), findsOneWidget);
|
||||
expect(find.text('My Title'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Should render empty container when empty string is provided',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: ItemTitle(itemTitle: ''),
|
||||
),
|
||||
);
|
||||
expect(find.byType(Container), findsOneWidget);
|
||||
expect(find.byType(Text), findsNothing);
|
||||
});
|
||||
}
|
||||
4
app/web/_headers
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
Referrer-Policy: no-referrer
|
||||
@@ -1,13 +1,15 @@
|
||||
import { downloads } from "@/helpers/helpers";
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
import { generalMetadata } from "@/helpers/metadata";
|
||||
import { generalMetadata, generalViewport } from "@/helpers/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
...generalMetadata,
|
||||
title: "FeedDeck - Download",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = generalViewport;
|
||||
|
||||
export default function Download() {
|
||||
return (
|
||||
<main>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ReactNode, useState } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { generalMetadata } from "@/helpers/metadata";
|
||||
import { generalMetadata, generalViewport } from "@/helpers/metadata";
|
||||
import GetStartedEntry from "@/components/getstartedentry";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -11,6 +11,8 @@ export const metadata: Metadata = {
|
||||
title: "FeedDeck - Support",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = generalViewport;
|
||||
|
||||
export default function Support() {
|
||||
return (
|
||||
<main>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { generalMetadata } from "@/helpers/metadata";
|
||||
import { generalMetadata, generalViewport } from "@/helpers/metadata";
|
||||
import GetStartedEntry from "@/components/getstartedentry";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -11,6 +11,8 @@ export const metadata: Metadata = {
|
||||
title: "FeedDeck - Support",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = generalViewport;
|
||||
|
||||
export default function Support() {
|
||||
return (
|
||||
<main>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import Image from "next/image";
|
||||
import dynamic from "next/dynamic";
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
const Download = dynamic(() => import("@/components/download"), { ssr: false });
|
||||
import { generalMetadata } from "@/helpers/metadata";
|
||||
import { generalMetadata, generalViewport } from "@/helpers/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
...generalMetadata,
|
||||
title: "FeedDeck - Follow your RSS and Social Media Feeds",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = generalViewport;
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
@@ -47,6 +49,7 @@ export default function Home() {
|
||||
src="/hero-1.webp"
|
||||
width="616"
|
||||
height="616"
|
||||
style={{ width: "auto", height: "auto" }}
|
||||
className="object-cover"
|
||||
alt="Hero"
|
||||
loading="eager"
|
||||
@@ -62,17 +65,17 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-20 mt-10">
|
||||
<FourChan />
|
||||
<GitHub />
|
||||
<GoogleNews />
|
||||
<Lemmy />
|
||||
<Mastodon />
|
||||
<Medium />
|
||||
<Nitter />
|
||||
<Pinterest />
|
||||
<Reddit />
|
||||
<RSS />
|
||||
<StackOverflow />
|
||||
<Tumblr />
|
||||
<X />
|
||||
<YouTube />
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,33 +85,37 @@ export default function Home() {
|
||||
<Feature
|
||||
title="Deck View"
|
||||
description="View all your RSS and social media feeds in a deck layout."
|
||||
image="/feature-1.webp"
|
||||
imageDesktop="/feature-1-desktop.webp"
|
||||
imageMobile="/feature-1-mobile.webp"
|
||||
/>
|
||||
<Feature
|
||||
title="Details View"
|
||||
description="View the details of all your RSS and social media items."
|
||||
image="/feature-2.webp"
|
||||
imageDesktop="/feature-2-desktop.webp"
|
||||
imageMobile="/feature-2-mobile.webp"
|
||||
/>
|
||||
<Feature
|
||||
title="YouTube"
|
||||
description="Follow and view your favorite YouTube channels."
|
||||
image="/feature-3.webp"
|
||||
imageDesktop="/feature-3-desktop.webp"
|
||||
imageMobile="/feature-3-mobile.webp"
|
||||
/>
|
||||
<Feature
|
||||
title="Podcasts"
|
||||
description="Follow and listen to your favorite podcasts, via the built-in podcast player."
|
||||
image="/feature-4.webp"
|
||||
imageDesktop="/feature-4-desktop.webp"
|
||||
imageMobile="/feature-4-mobile.webp"
|
||||
/>
|
||||
<Feature
|
||||
title="GitHub"
|
||||
description="View your GitHub notifications and activities of your favorite repositories."
|
||||
image="/feature-5.webp"
|
||||
imageDesktop="/feature-5-desktop.webp"
|
||||
imageMobile="/feature-5-mobile.webp"
|
||||
/>
|
||||
<Feature
|
||||
<FeatureNoBg
|
||||
title="Available on all Platforms"
|
||||
description="The same experience on all your devices."
|
||||
image="/feature-6.webp"
|
||||
noBg={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -132,11 +139,11 @@ export default function Home() {
|
||||
}
|
||||
|
||||
const Feature = (
|
||||
{ title, description, image, noBg }: {
|
||||
{ title, description, imageDesktop, imageMobile }: {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
noBg?: boolean;
|
||||
imageDesktop: string;
|
||||
imageMobile: string;
|
||||
},
|
||||
) => (
|
||||
<div className="container mx-auto flex flex-col justify-center p-16 items-center text-center">
|
||||
@@ -146,7 +153,39 @@ const Feature = (
|
||||
<div className="mb-5 font-light text-gray-400 sm:text-xl">
|
||||
{description}
|
||||
</div>
|
||||
<div className={noBg ? "p-2 rounded-lg" : "p-2 bg-secondary rounded-lg"}>
|
||||
<div className="p-2 bg-secondary rounded-lg">
|
||||
<picture>
|
||||
<source media="(max-width: 640px)" srcSet={imageMobile} />
|
||||
<source media="(min-width: 640px)" srcSet={imageDesktop} />
|
||||
<Image
|
||||
src={imageDesktop}
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "100%", height: "auto" }}
|
||||
className="object-cover"
|
||||
alt="Feature"
|
||||
loading="eager"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const FeatureNoBg = (
|
||||
{ title, description, image }: {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
},
|
||||
) => (
|
||||
<div className="container mx-auto flex flex-col justify-center p-16 items-center text-center">
|
||||
<div className="mb-2 text-4xl tracking-tight font-extrabold">
|
||||
{title}
|
||||
</div>
|
||||
<div className="mb-5 font-light text-gray-400 sm:text-xl">
|
||||
{description}
|
||||
</div>
|
||||
<div className="p-2 rounded-lg">
|
||||
<Image
|
||||
src={image}
|
||||
width={0}
|
||||
@@ -160,6 +199,24 @@ const Feature = (
|
||||
</div>
|
||||
);
|
||||
|
||||
const FourChan = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
width="32px"
|
||||
height="32px"
|
||||
viewBox="0 0 4096 4096"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<g id="_4chan" transform="matrix(4.26802,0,0,4.26802,-137.229,-137.223)">
|
||||
<path d="M474.83,384.842C474.83,384.842 424.165,75.302 237.905,76.022C112.383,76.502 63.359,197.062 154.333,224.174C154.333,224.174 46.448,263.082 46.448,332.019C46.448,400.996 217.353,473.573 474.828,384.801L474.83,384.842ZM552.045,621.726C552.045,621.726 588.193,933.266 774.293,941.263C899.693,946.661 954.355,828.499 864.743,797.15C864.743,797.15 974.308,763.358 977.508,694.503C980.708,625.605 813.361,545.071 552.046,621.727L552.045,621.726ZM405.212,559.744C405.212,559.744 104.028,647.316 127.222,832.176C142.817,956.698 268.377,990.886 284.372,897.356C284.372,897.356 335.955,999.684 404.412,991.366C472.869,983.048 524.374,804.626 405.212,559.746L405.212,559.744ZM630.06,461.098C630.06,461.098 939.04,407.115 936.364,220.856C934.52,95.376 813.44,47.592 787.29,138.843C787.29,138.843 747.302,31.438 678.284,32.155C609.344,32.877 538.569,204.581 630.06,461.098Z" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="pt-4">4chan</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const GitHub = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
@@ -199,6 +256,24 @@ const GoogleNews = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const Lemmy = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
width="32px"
|
||||
height="32px"
|
||||
viewBox="0 0 4096 4096"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<g id="Lemmy" transform="matrix(170.666,0,0,170.666,-0.0042009,-0.0031286)">
|
||||
<path d="M2.959,4.223C2.849,4.224 2.738,4.231 2.627,4.242C1.749,4.343 0.958,4.812 0.473,5.628C-0.002,6.428 -0.12,7.309 0.123,8.125C0.365,8.941 0.952,9.683 1.821,10.27C1.826,10.274 1.831,10.278 1.837,10.281C2.583,10.731 3.329,11.025 4.125,11.181C4.105,11.513 4.098,11.85 4.119,12.199C4.182,13.242 4.555,14.2 5.115,15.048L3.109,15.867C3.006,15.908 2.924,15.989 2.881,16.091C2.815,16.246 2.85,16.426 2.969,16.546C3.047,16.624 3.153,16.669 3.263,16.669C3.318,16.669 3.372,16.659 3.422,16.638L5.632,15.734C6.04,16.22 6.501,16.666 7.004,17.052C7.024,17.067 7.045,17.079 7.064,17.095L5.773,18.805C5.722,18.876 5.694,18.961 5.694,19.048C5.694,19.276 5.882,19.464 6.11,19.464C6.237,19.464 6.358,19.406 6.437,19.306L7.751,17.567C8.469,18.022 9.229,18.36 9.98,18.592C10.363,19.313 11.118,19.779 12,19.779C12.891,19.779 13.644,19.278 14.024,18.572C14.768,18.337 15.52,17.999 16.232,17.545L17.564,19.306C17.642,19.41 17.765,19.472 17.896,19.472C18.103,19.472 18.279,19.317 18.308,19.112C18.323,19.003 18.294,18.893 18.228,18.805L16.918,17.07C16.926,17.063 16.936,17.059 16.944,17.052C17.441,16.671 17.899,16.234 18.306,15.758L20.461,16.637C20.672,16.723 20.916,16.62 21.002,16.41C21.023,16.359 21.034,16.305 21.034,16.251C21.034,16.082 20.931,15.93 20.775,15.866L18.831,15.074C19.408,14.22 19.801,13.255 19.882,12.204C19.908,11.854 19.906,11.513 19.89,11.178C20.68,11.021 21.422,10.728 22.163,10.281C22.169,10.277 22.174,10.273 22.18,10.269C23.048,9.682 23.636,8.941 23.878,8.124C24.12,7.308 24.003,6.427 23.528,5.627C23.043,4.811 22.251,4.342 21.373,4.241C21.262,4.229 21.152,4.223 21.041,4.222C20.255,4.207 19.418,4.451 18.612,4.916C18.019,5.258 17.487,5.783 17.069,6.355C15.899,5.685 14.376,5.307 12.505,5.277C12.335,5.274 12.164,5.274 11.994,5.277C9.909,5.311 8.24,5.707 6.98,6.421C6.977,6.418 6.975,6.41 6.972,6.406C6.549,5.815 5.999,5.27 5.388,4.917C4.583,4.452 3.745,4.209 2.96,4.223L2.959,4.223ZM3.245,4.959C3.802,5.001 4.415,5.195 5.024,5.547C5.509,5.827 6,6.302 6.37,6.816C6.197,6.943 6.031,7.079 5.873,7.225C4.953,8.077 4.412,9.185 4.205,10.458C3.524,10.318 2.887,10.064 2.221,9.664C1.466,9.151 1.005,8.541 0.821,7.919C0.636,7.295 0.713,6.648 1.098,6C1.473,5.37 2.026,5.046 2.71,4.967C2.888,4.948 3.067,4.945 3.245,4.96L3.245,4.959ZM20.755,4.959C20.933,4.945 21.112,4.947 21.29,4.967C21.974,5.045 22.527,5.369 22.902,5.999C23.287,6.647 23.364,7.295 23.179,7.919C22.995,8.541 22.534,9.15 21.779,9.663C21.121,10.059 20.491,10.31 19.819,10.452C19.625,9.155 19.082,8.018 18.153,7.15C18.003,7.011 17.846,6.88 17.682,6.758C18.047,6.268 18.511,5.815 18.976,5.546C19.585,5.195 20.198,5.001 20.755,4.959L20.755,4.959ZM12.006,6.004C12.168,6.001 12.331,6.001 12.493,6.004C14.883,6.043 16.578,6.674 17.656,7.682C18.806,8.757 19.298,10.282 19.155,12.149C19.024,13.856 17.935,15.377 16.503,16.473C15.809,17.004 15.041,17.408 14.278,17.702C14.281,17.657 14.292,17.613 14.292,17.567C14.295,16.371 13.36,15.354 12,15.354C10.639,15.354 9.678,16.369 9.708,17.575C9.709,17.626 9.722,17.672 9.726,17.722C8.945,17.429 8.157,17.021 7.448,16.477C6.025,15.382 4.949,13.864 4.845,12.155C4.732,10.298 5.223,8.816 6.366,7.758C7.509,6.7 9.352,6.047 12.006,6.004ZM8.364,12.833C7.759,12.833 7.268,13.323 7.268,13.929C7.268,14.536 7.759,15.028 8.364,15.028C8.969,15.028 9.461,14.536 9.461,13.929C9.461,13.323 8.969,12.833 8.364,12.833ZM15.646,12.844C15.046,12.844 14.559,13.33 14.559,13.931C14.559,14.531 15.046,15.019 15.646,15.019C16.246,15.019 16.733,14.531 16.733,13.931C16.733,13.33 16.246,12.844 15.646,12.844ZM12,16.084C13.024,16.084 13.565,16.722 13.563,17.566C13.562,18.351 12.891,19.051 12,19.051C11.083,19.051 10.46,18.489 10.437,17.558C10.415,16.724 10.976,16.084 12,16.084Z" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="pt-4">Lemmy</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Mastodon = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
@@ -235,24 +310,6 @@ const Medium = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const Nitter = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
width="32px"
|
||||
height="32px"
|
||||
viewBox="0 0 4096 4096"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<g id="Nitter" transform="matrix(7.0137,0,0,7.0137,-289.9,-289.9)">
|
||||
<path d="M98.133,44.8L94.667,48.4L94.667,618.267L98.133,621.867L101.733,625.334L218.267,625.334L221.867,621.867L225.333,618.267L225.333,453.2C225.333,316.4 225.6,288 227.067,288C228.133,288 281.867,362.8 346.667,454.267C411.334,545.867 465.6,621.734 467.2,622.934C469.734,625.067 474.534,625.334 517.467,625.334L564.934,625.334L568.534,621.867L572,618.267L572,48.4L568.534,44.8L564.934,41.333L448.4,41.333L444.8,44.8L441.334,48.4L441.334,213.467C441.334,350.267 441.067,378.667 439.6,378.667C438.534,378.667 384.8,303.867 320,212.267C255.333,120.8 201.067,44.933 199.467,43.733C196.933,41.6 192.133,41.333 149.2,41.333L101.733,41.333L98.133,44.8ZM310,240.933C378.267,337.334 435.867,418.4 438.134,420.8C442,424.934 442.934,425.334 450.267,425.334C457.2,425.334 458.8,424.8 461.867,421.867L465.334,418.267L465.334,65.333L548,65.333L548,601.334L514.4,601.2L480.667,601.2L356.667,425.734C288.533,329.333 230.8,248.267 228.533,245.867C224.667,241.733 223.733,241.333 216.4,241.333C209.467,241.333 207.867,241.867 204.8,244.8L201.333,248.4L201.333,601.334L118.667,601.334L118.667,65.333L152.4,65.467L186,65.467L310,240.933Z" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="pt-4">Nitter</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Pinterest = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
@@ -346,24 +403,6 @@ const Tumblr = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const X = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
width="32px"
|
||||
height="32px"
|
||||
viewBox="0 0 4096 4096"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<g id="X" transform="matrix(8.90048,0,0,8.90048,-238.533,-230.522)">
|
||||
<path d="M389.2,48L459.8,48L305.6,224.2L487,464L345,464L233.7,318.6L106.5,464L35.8,464L200.7,275.5L26.8,48L172.4,48L272.9,180.9L389.2,48ZM364.4,421.8L403.5,421.8L151.1,88L109.1,88L364.4,421.8Z" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="pt-4">X</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const YouTube = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
import { generalMetadata } from "@/helpers/metadata";
|
||||
import { generalMetadata, generalViewport } from "@/helpers/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
...generalMetadata,
|
||||
title: "FeedDeck - Pricing",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = generalViewport;
|
||||
|
||||
export default function Pricing() {
|
||||
return (
|
||||
<main>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
import { generalMetadata } from "@/helpers/metadata";
|
||||
import { generalMetadata, generalViewport } from "@/helpers/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
...generalMetadata,
|
||||
title: "FeedDeck - Privacy Policy",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = generalViewport;
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
return (
|
||||
<main>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
import { generalMetadata } from "@/helpers/metadata";
|
||||
import { generalMetadata, generalViewport } from "@/helpers/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
...generalMetadata,
|
||||
title: "FeedDeck - Support",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = generalViewport;
|
||||
|
||||
export default function Support() {
|
||||
return (
|
||||
<main>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
import { generalMetadata } from "@/helpers/metadata";
|
||||
import { generalMetadata, generalViewport } from "@/helpers/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
...generalMetadata,
|
||||
title: "FeedDeck - Terms & Conditions",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = generalViewport;
|
||||
|
||||
export default function TermsAndConditions() {
|
||||
return (
|
||||
<main>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
export const generalMetadata: Metadata = {
|
||||
description: "Follow your RSS and Social Media Feeds",
|
||||
@@ -7,12 +7,11 @@ export const generalMetadata: Metadata = {
|
||||
name: "Rico Berger",
|
||||
},
|
||||
keywords: ["FeedDeck", "RSS", "Social Media", "Feeds"],
|
||||
themeColor: "#1f2229",
|
||||
metadataBase: process.env.NEXT_PUBLIC_METADATA_BASE
|
||||
? new URL(process.env.NEXT_PUBLIC_METADATA_BASE)
|
||||
: process.env.NODE_ENV === "development"
|
||||
? new URL("http://localhost:3000")
|
||||
: new URL("https://feeddeck.app"),
|
||||
? new URL("http://localhost:3000")
|
||||
: new URL("https://feeddeck.app"),
|
||||
icons: [
|
||||
{
|
||||
rel: "apple-touch-icon",
|
||||
@@ -112,3 +111,7 @@ export const generalMetadata: Metadata = {
|
||||
"msApplication-PackageFamilyName": "26077RicoBerger.FeedDeck_2w82je6nmmv2c",
|
||||
},
|
||||
};
|
||||
|
||||
export const generalViewport: Viewport = {
|
||||
themeColor: "#1f2229",
|
||||
}
|
||||
|
||||
2423
landing/package-lock.json
generated
@@ -9,18 +9,18 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/react": "^18.2.33",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-next": "^14.0.1",
|
||||
"next": "^14.0.1",
|
||||
"postcss": "^8.4.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.1.3"
|
||||
"@headlessui/react": "^2.1.2",
|
||||
"@types/node": "^22.0.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.2.5",
|
||||
"next": "^14.2.5",
|
||||
"postcss": "^8.4.40",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
landing/public/android-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.8 KiB |
BIN
landing/public/android-icon-36x36.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
landing/public/android-icon-48x48.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
landing/public/android-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
landing/public/android-icon-96x96.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 575 KiB After Width: | Height: | Size: 575 KiB |
BIN
landing/public/feature-1-mobile.webp
Normal file
|
After Width: | Height: | Size: 204 KiB |