Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bac50cb86e | ||
|
|
3e43029b96 | ||
|
|
b3e82b7a8e | ||
|
|
2d3622487d | ||
|
|
683f5b3369 | ||
|
|
01250cfc35 | ||
|
|
e726b4b0ea | ||
|
|
648ed898d9 | ||
|
|
a563562edb | ||
|
|
a16599ef96 | ||
|
|
30a69f4174 | ||
|
|
008bf0bf6e | ||
|
|
1dde1d2635 | ||
|
|
f9e1526d54 | ||
|
|
1e2993305c | ||
|
|
cc36e2a89a | ||
|
|
e8c0048229 | ||
|
|
6a1a21eec3 | ||
|
|
2d838240fd | ||
|
|
3a8308b66c | ||
|
|
a5ed230f90 | ||
|
|
7b37aa749c | ||
|
|
74b11075cd | ||
|
|
7d85dff1f7 | ||
|
|
9ef3ce60ca | ||
|
|
eb331f1540 | ||
|
|
4b9683d5b6 | ||
|
|
8fc68951b0 | ||
|
|
2270943066 | ||
|
|
c551d1635a | ||
|
|
f100925eff | ||
|
|
48f504ede9 | ||
|
|
b8fdbf17a2 | ||
|
|
6a3b481219 | ||
|
|
bf95846e6c | ||
|
|
2c712f6a07 | ||
|
|
61a30eaa06 | ||
|
|
a9b4bef96f | ||
|
|
4e0296e226 | ||
|
|
f4c755e131 | ||
|
|
ad0d214528 | ||
|
|
965c647b07 | ||
|
|
2094b36384 | ||
|
|
394e64d26b | ||
|
|
a7c134e53c | ||
|
|
6cc79ef8c6 | ||
|
|
842c78bf1d | ||
|
|
0218342f46 | ||
|
|
432118ae47 | ||
|
|
f481f2ceb9 | ||
|
|
d768b0ffdf | ||
|
|
a0229d8ece | ||
|
|
50ad24a7bc | ||
|
|
76854e72df | ||
|
|
88bbb9f56f | ||
|
|
fb97f818b3 | ||
|
|
2e3110e298 | ||
|
|
a167d94165 | ||
|
|
614a1b9ab9 | ||
|
|
8f2008a179 | ||
|
|
0cc98b85b1 | ||
|
|
b34497a6bc | ||
|
|
504b88510c | ||
|
|
481b5c6490 | ||
|
|
10278aefbb | ||
|
|
9117344e82 | ||
|
|
bb4a380a7c | ||
|
|
62ac6d1ade | ||
|
|
22ebd600b9 | ||
|
|
452177f993 | ||
|
|
a6a54ac02e | ||
|
|
96eccb5503 | ||
|
|
43a8ebe513 | ||
|
|
63f7ab7bf6 | ||
|
|
676ff316d7 | ||
|
|
3eccb0bd4a | ||
|
|
351c61f613 | ||
|
|
333e762539 | ||
|
|
280a222210 | ||
|
|
4853eeb0a8 | ||
|
|
9e9bf01b5b | ||
|
|
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 |
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @ricoberger
|
||||
1
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
||||
---
|
||||
github: [ricoberger]
|
||||
custom: ["https://www.paypal.me/ricoberger"]
|
||||
|
||||
BIN
.github/assets/badge-app-store.png
vendored
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
.github/assets/badge-flathub.png
vendored
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
.github/assets/badge-google-play.png
vendored
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
.github/assets/badge-mac-app-store.png
vendored
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
.github/assets/badge-web.png
vendored
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
.github/assets/badge-windows-store.png
vendored
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
25
.github/dependabot.yml
vendored
@@ -1,9 +1,14 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
labels:
|
||||
- "changelog: changed"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
@@ -13,6 +18,10 @@ updates:
|
||||
directory: "/app"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
labels:
|
||||
- "changelog: changed"
|
||||
groups:
|
||||
pub:
|
||||
patterns:
|
||||
@@ -22,6 +31,10 @@ updates:
|
||||
directory: "/supabase/functions/_cmd"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
labels:
|
||||
- "changelog: changed"
|
||||
groups:
|
||||
docker:
|
||||
patterns:
|
||||
@@ -31,8 +44,12 @@ updates:
|
||||
directory: "/landing"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
labels:
|
||||
- "changelog: changed"
|
||||
groups:
|
||||
npm-landing:
|
||||
npm:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
@@ -40,7 +57,11 @@ updates:
|
||||
directory: "/supabase/email-templates"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
labels:
|
||||
- "changelog: changed"
|
||||
groups:
|
||||
npm-email-templates:
|
||||
npm:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
9
.github/release.yaml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name-template: "$RESOLVED_VERSION"
|
||||
tag-template: "$RESOLVED_VERSION"
|
||||
version-template: "v$MAJOR.$MINOR.$PATCH"
|
||||
@@ -15,15 +16,15 @@ version-resolver:
|
||||
minor:
|
||||
labels:
|
||||
- "changelog: added"
|
||||
- "changelog: changed"
|
||||
patch:
|
||||
labels:
|
||||
- "changelog: changed"
|
||||
- "changelog: fixed"
|
||||
default: patch
|
||||
category-template: "### $TITLE"
|
||||
change-template: '- #$NUMBER: $TITLE @$AUTHOR'
|
||||
change-template: "- #$NUMBER: $TITLE @$AUTHOR"
|
||||
template: |
|
||||
$CHANGES
|
||||
replacers:
|
||||
- search: ':warning:'
|
||||
replace: ':warning: _Breaking change:_ :warning:'
|
||||
- search: ":warning:"
|
||||
replace: ":warning: _Breaking change:_ :warning:"
|
||||
|
||||
365
.github/workflows/continuous-delivery.yaml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Continuous Delivery
|
||||
|
||||
on:
|
||||
@@ -5,35 +6,41 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
# The "Docker" job builds the Docker image and pushes it to the GitHub Container Registry. The job only runs when a
|
||||
# commit is pushed to the main branch or a new tag is created.
|
||||
# The "Docker" job builds the Docker image and pushes it to the GitHub
|
||||
# Container Registry. The job only runs when a commit is pushed to the main
|
||||
# branch or a new tag is created.
|
||||
docker:
|
||||
name: Docker
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
if:
|
||||
github.ref == 'refs/heads/main' || (github.event_name == 'release' &&
|
||||
github.event.action == 'published')
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Docker Tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo TAG=${GITHUB_REF:10} >> $GITHUB_ENV
|
||||
else
|
||||
echo TAG=main >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Docker Metadata
|
||||
id: metadata
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{raw}}
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -41,7 +48,7 @@ jobs:
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -50,26 +57,30 @@ 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
|
||||
file: ./supabase/functions/_cmd/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: ghcr.io/${{ github.repository_owner }}/feeddeck:${{ env.TAG }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
|
||||
# The "Supabase" job runs the database migrations and deploys all Supabase functions. The job only runs when a commit
|
||||
# is pushed to the main branch or a new tag is created.
|
||||
# The "Supabase" job runs the database migrations and deploys all Supabase
|
||||
# functions. The job only runs when a commit is pushed to the main branch or
|
||||
# a new tag is created.
|
||||
supabase:
|
||||
name: Supabase
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
if:
|
||||
github.ref == 'refs/heads/main' || (github.event_name == 'release' &&
|
||||
github.event.action == 'published')
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -87,19 +98,22 @@ jobs:
|
||||
|
||||
supabase db push
|
||||
|
||||
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-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
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref $PROJECT_ID
|
||||
# supabase functions deploy add-source-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy delete-user-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy image-proxy-v1 --no-verify-jwt --project-ref $PROJECT_ID
|
||||
# supabase functions deploy profile-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy profile-v2 --project-ref $PROJECT_ID
|
||||
supabase functions deploy revenuecat-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID
|
||||
supabase functions deploy stripe-create-billing-portal-link-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID
|
||||
|
||||
- name: Push Database Migration and Deploy Functions
|
||||
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||
if:
|
||||
${{ github.event_name == 'release' && github.event.action ==
|
||||
'published' }}
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_PROD_DB_PASSWORD }}
|
||||
@@ -109,23 +123,30 @@ jobs:
|
||||
|
||||
supabase db push
|
||||
|
||||
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-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
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy add-source-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy delete-user-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy image-proxy-v1 --no-verify-jwt --project-ref $PROJECT_ID
|
||||
supabase functions deploy profile-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy profile-v2 --project-ref $PROJECT_ID
|
||||
supabase functions deploy revenuecat-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID
|
||||
supabase functions deploy stripe-create-billing-portal-link-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref $PROJECT_ID
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID
|
||||
|
||||
# The "Web" job builds the Flutter web app and publishes it to Cloudflare Pages. The job only runs when a commit is
|
||||
# pushed to the main branch or a new tag is created.
|
||||
# The "Web" job builds the Flutter web app and publishes it to Cloudflare
|
||||
# Pages. The job only runs on pull requests or when a commit is pushed to the
|
||||
# main branch or a new tag is created.
|
||||
#
|
||||
# When the job runs on a pull request it only builds the app but doesn't
|
||||
# upload the build to Cloudflare.
|
||||
web:
|
||||
name: Web
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
if:
|
||||
github.event_name == 'pull_request' || github.ref == 'refs/heads/main' ||
|
||||
(github.event_name == 'release' && github.event.action == 'published')
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
@@ -134,7 +155,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -166,11 +187,12 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
channel: 'stable'
|
||||
flutter-version: "3.32.7"
|
||||
channel: "stable"
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
|
||||
cache-path:
|
||||
"${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
@@ -183,6 +205,9 @@ jobs:
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
if:
|
||||
github.ref == 'refs/heads/main' || (github.event_name == 'release' &&
|
||||
github.event.action == 'published')
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
@@ -190,12 +215,15 @@ jobs:
|
||||
directory: ./app/build/web
|
||||
branch: main
|
||||
|
||||
# The "macOS" job builds the Flutter macOS 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.
|
||||
# The "macOS" job builds the Flutter macOS 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.
|
||||
macos:
|
||||
name: macOS
|
||||
runs-on: macos-latest
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
runs-on: macos-14
|
||||
if:
|
||||
github.event_name == 'pull_request' || (github.event_name == 'release' &&
|
||||
github.event.action == 'published')
|
||||
permissions:
|
||||
contents: write
|
||||
defaults:
|
||||
@@ -204,16 +232,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
channel: 'stable'
|
||||
flutter-version: "3.32.7"
|
||||
channel: "stable"
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
|
||||
cache-path:
|
||||
"${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
@@ -222,6 +251,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
flutter config --enable-macos-desktop
|
||||
flutter config --enable-swift-package-manager
|
||||
FLUTTER_XCODE_CODE_SIGN_IDENTITY="" FLUTTER_XCODE_CODE_SIGNING_REQUIRED=NO flutter build macos --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
|
||||
@@ -230,7 +260,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
|
||||
@@ -238,17 +268,22 @@ jobs:
|
||||
|
||||
- name: Upload Artifacts (Release)
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||
if:
|
||||
${{ github.event_name == 'release' && github.event.action ==
|
||||
'published' }}
|
||||
with:
|
||||
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')
|
||||
if:
|
||||
github.event_name == 'pull_request' || (github.event_name == 'release' &&
|
||||
github.event.action == 'published')
|
||||
permissions:
|
||||
contents: write
|
||||
defaults:
|
||||
@@ -257,25 +292,27 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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:
|
||||
# Required for Package "media_kit" which is used via
|
||||
# "just_audio_media_kit" for Linux and Windows:
|
||||
# See: https://pub.dev/packages/media_kit and https://pub.dev/packages/just_audio_media_kit
|
||||
sudo apt-get install -y libmpv-dev mpv
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
channel: 'stable'
|
||||
flutter-version: "3.32.7"
|
||||
channel: "stable"
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
|
||||
cache-path:
|
||||
"${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
@@ -297,7 +334,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
|
||||
@@ -305,17 +342,26 @@ jobs:
|
||||
|
||||
- name: Upload Artifacts (Release)
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||
if:
|
||||
${{ github.event_name == 'release' && github.event.action ==
|
||||
'published' }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: app/build/feeddeck-linux-x86_64.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:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
# 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:
|
||||
@@ -324,16 +370,91 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Packages
|
||||
run: |
|
||||
# Required for Flutter
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
# Required for Package "media_kit" which is used via
|
||||
# "just_audio_media_kit" for Linux and Windows:
|
||||
# See: https://pub.dev/packages/media_kit and https://pub.dev/packages/just_audio_media_kit
|
||||
sudo apt-get install -y libmpv-dev mpv
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
channel: 'stable'
|
||||
flutter-version: "3.32.7"
|
||||
channel: "master"
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
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:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
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@v5
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.32.7"
|
||||
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: |
|
||||
@@ -343,50 +464,76 @@ jobs:
|
||||
run: |
|
||||
flutter config --enable-windows-desktop
|
||||
flutter build windows --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: Create Archive
|
||||
run: |
|
||||
Compress-Archive -Path build/windows/x64/runner/Release/* -Destination feeddeck-windows-x86_64.zip
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
flutter pub run msix:create --output-path build --output-name feeddeck
|
||||
cd build
|
||||
7z a -tzip feeddeck-windows-x86_64.zip feeddeck.msix
|
||||
7z a -tzip feeddeck-windows-x86_64-msix.zip feeddeck.msix
|
||||
|
||||
- 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
|
||||
path: app/feeddeck-windows-x86_64.zip
|
||||
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' }}
|
||||
if:
|
||||
${{ github.event_name == 'release' && github.event.action ==
|
||||
'published' }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: app/build/feeddeck-windows-x86_64.zip
|
||||
asset_path: app/feeddeck-windows-x86_64.zip
|
||||
|
||||
# The "iOS" job builds the Flutter iOS app on every pull request. This is only used to test that the build of the iOS
|
||||
# app works. The artifact of the build isn't uploaded / used.
|
||||
- name: Upload Artifacts (PR)
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: feeddeck-windows-x86_64-msix.zip
|
||||
path: app/build/feeddeck-windows-x86_64-msix.zip
|
||||
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-windows-x86_64-msix.zip
|
||||
|
||||
# The "iOS" job builds the Flutter iOS app on every pull request. This is only
|
||||
# used to test that the build of the iOS app works. The artifact of the build
|
||||
# isn't uploaded / used.
|
||||
ios:
|
||||
name: iOS
|
||||
runs-on: macos-latest
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
runs-on: macos-14
|
||||
if:
|
||||
github.event_name == 'pull_request' || (github.event_name == 'release' &&
|
||||
github.event.action == 'published')
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "app"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
channel: 'stable'
|
||||
flutter-version: "3.32.7"
|
||||
channel: "stable"
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
|
||||
cache-path:
|
||||
"${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
@@ -395,30 +542,38 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
flutter config --enable-ios
|
||||
flutter config --enable-swift-package-manager
|
||||
flutter build ipa --no-codesign --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
|
||||
|
||||
# The "Android" job builds the Flutter Android app on every pull request. This is only used to test that the build of
|
||||
# the Android app works. The artifact of the build isn't uploaded / used.
|
||||
# The "Android" job builds the Flutter Android app on every pull request. This
|
||||
# is only used to test that the build of the Android app works. The artifact
|
||||
# of the build isn't uploaded / used.
|
||||
android:
|
||||
name: Android
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
if:
|
||||
github.event_name == 'pull_request' || (github.event_name == 'release' &&
|
||||
github.event.action == 'published')
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "app"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- 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'
|
||||
channel: 'stable'
|
||||
flutter-version: "3.32.7"
|
||||
channel: "stable"
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
|
||||
cache-path:
|
||||
"${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
|
||||
89
.github/workflows/continuous-integration.yaml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
flutter:
|
||||
name: Flutter
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "app"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.32.7"
|
||||
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: Lint
|
||||
run: |
|
||||
dart analyze --fatal-infos
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
flutter test
|
||||
|
||||
deno:
|
||||
name: Deno
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
FEEDDECK_SUPABASE_URL: http://localhost:54321
|
||||
FEEDDECK_SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
|
||||
FEEDDECK_LOG_LEVEL: debug
|
||||
FEEDDECK_SUPABASE_SERVICE_ROLE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Supabase
|
||||
uses: supabase/setup-cli@v1
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.45.2
|
||||
|
||||
- name: Start Supabase
|
||||
run: |
|
||||
echo "FEEDDECK_LOG_LEVEL=${FEEDDECK_LOG_LEVEL}" >> ./supabase/.env.test
|
||||
echo "FEEDDECK_SUPABASE_URL=${FEEDDECK_SUPABASE_URL}" >> ./supabase/.env.test
|
||||
echo "FEEDDECK_SUPABASE_ANON_KEY=${FEEDDECK_SUPABASE_ANON_KEY}" >> ./supabase/.env.test
|
||||
echo "FEEDDECK_SUPABASE_SERVICE_ROLE_KEY=${FEEDDECK_SUPABASE_SERVICE_ROLE_KEY}" >> ./supabase/.env.test
|
||||
|
||||
supabase start
|
||||
supabase db reset
|
||||
supabase functions serve --no-verify-jwt --env-file supabase/.env.test &
|
||||
|
||||
psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "UPDATE settings SET value='http://kong:8000' WHERE name='supabase_api_url'"
|
||||
psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "UPDATE settings SET value='${FEEDDECK_SUPABASE_SERVICE_ROLE_KEY}' WHERE name='supabase_service_role_key'"
|
||||
|
||||
- name: Lint
|
||||
working-directory: "supabase/functions"
|
||||
run: |
|
||||
deno task lint
|
||||
|
||||
- name: Test
|
||||
working-directory: "supabase/functions"
|
||||
run: |
|
||||
deno task test
|
||||
13
.github/workflows/release.yaml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Release
|
||||
|
||||
on:
|
||||
@@ -16,7 +17,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
|
||||
@@ -37,17 +38,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
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 +59,7 @@ jobs:
|
||||
npm run build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: ./landing/out
|
||||
|
||||
@@ -78,4 +79,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"
|
||||
}
|
||||
}
|
||||
212
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.29.2 • channel stable • https://github.com/flutter/flutter.git
|
||||
Framework • revision c236373904 (2 weeks ago) • 2025-03-13 16:17:06 -0400
|
||||
Engine • revision 18b71d647a
|
||||
Tools • Dart 3.7.2 • DevTools 2.42.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:latest 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:latest supabase/functions
|
||||
|
||||
# The Docker image can then be used to run the scheduler, worker or tools, e.g.
|
||||
docker run ghcr.io/feeddeck/feeddeck:latest 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 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 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,16 +283,17 @@ supabase secrets set --env-file supabase/.env
|
||||
supabase secrets list
|
||||
|
||||
# Deploy all functions
|
||||
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-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
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref <PROJECT-ID>
|
||||
supabase functions deploy add-source-v1 --project-ref <PROJECT-ID>
|
||||
supabase functions deploy delete-user-v1 --project-ref <PROJECT-ID>
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref <PROJECT-ID>
|
||||
supabase functions deploy image-proxy-v1 --no-verify-jwt --project-ref <PROJECT-ID>
|
||||
supabase functions deploy profile-v1 --project-ref <PROJECT-ID>
|
||||
supabase functions deploy profile-v2 --project-ref <PROJECT-ID>
|
||||
supabase functions deploy revenuecat-webhooks-v1 --no-verify-jwt --project-ref <PROJECT-ID>
|
||||
supabase functions deploy stripe-create-billing-portal-link-v1 --project-ref <PROJECT-ID>
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref <PROJECT-ID>
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref <PROJECT-ID>
|
||||
```
|
||||
|
||||
Now we have to do some manual steps to finish the setup of our Supabase project:
|
||||
@@ -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) 2026 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
|
||||
|
||||
29
README.md
@@ -1,22 +1,25 @@
|
||||
<div align="center">
|
||||
<img src=".github/assets/icon.png" width="200" />
|
||||
<br><br>
|
||||
<img src=".github/assets/icon.png" width="200" />
|
||||
<br><br>
|
||||
|
||||
**FeedDeck** is an open source RSS and social media feed reader, inspired by
|
||||
TweetDeck. FeedDeck allows you to follow your favorite feeds in one place on all
|
||||
platforms.
|
||||
|
||||
<p>
|
||||
<a href="https://apps.apple.com/us/app/feeddeck/id6451055362" target="_blank"><img src=".github/assets/badge-app-store.svg" height="50"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=app.feeddeck.feeddeck" target="_blank"><img src=".github/assets/badge-google-play.svg" height="50"></a>
|
||||
<a href="https://app.feeddeck.app" target="_blank"><img src=".github/assets/badge-web.svg" height="50"></a>
|
||||
<a href="https://apps.apple.com/us/app/feeddeck/id6451055362" target="_blank"><img src=".github/assets/badge-mac-app-store.svg" height="50"></a>
|
||||
<a href="https://www.microsoft.com/store/apps/9NPHPGRRCT5H" target="_blank"><img src=".github/assets/badge-windows-store.svg" height="50"></a>
|
||||
<a href="https://flathub.org/en/apps/app.feeddeck.feeddeck" target="_blank"><img src=".github/assets/badge-flathub.svg" height="50"></a>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://apps.apple.com/us/app/feeddeck/id6451055362" target="_blank"><img src=".github/assets/badge-app-store.png" height="50"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=app.feeddeck.feeddeck" target="_blank"><img src=".github/assets/badge-google-play.png" height="50"></a>
|
||||
<a href="https://app.feeddeck.app" target="_blank"><img src=".github/assets/badge-web.png" height="50"></a>
|
||||
<a href="https://apps.apple.com/us/app/feeddeck/id6451055362" target="_blank"><img src=".github/assets/badge-mac-app-store.png" height="50"></a>
|
||||
<a href="https://www.microsoft.com/store/apps/9NPHPGRRCT5H" target="_blank"><img src=".github/assets/badge-windows-store.png" height="50"></a>
|
||||
<a href="https://flathub.org/en/apps/app.feeddeck.feeddeck" target="_blank"><img src=".github/assets/badge-flathub.png" height="50"></a>
|
||||
</p>
|
||||
|
||||
<img src=".github/assets/screenshot.png" width="100%" />
|
||||
<br><br>
|
||||
<div align="center">
|
||||
<img src=".github/assets/screenshot.png" width="100%" />
|
||||
<br><br>
|
||||
</div>
|
||||
|
||||
**FeedDeck** is an open source RSS and social media feed reader, inspired by
|
||||
@@ -31,8 +34,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
|
||||
|
||||
8
app/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage
|
||||
3
app/.gitignore
vendored
@@ -5,9 +5,11 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
@@ -31,6 +33,7 @@ migrate_working_dir/
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
14
app/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM ghcr.io/cirruslabs/flutter:3.29.3 AS build
|
||||
|
||||
ARG SUPABASE_URL
|
||||
ARG SUPABASE_ANON_KEY
|
||||
ARG SUPABASE_SITE_URL
|
||||
ARG GOOGLE_CLIENT_ID
|
||||
|
||||
RUN mkdir /app/
|
||||
COPY . /app/
|
||||
WORKDIR /app/
|
||||
RUN flutter build web --release --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}
|
||||
|
||||
FROM nginx:1.26.3
|
||||
COPY --from=build /app/build/web /usr/share/nginx/html
|
||||
1
app/android/.gitignore
vendored
@@ -5,6 +5,7 @@ gradle-wrapper.jar
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
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'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
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 {
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "app.feeddeck.feeddeck"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
// minSdkVersion flutter.minSdkVersion
|
||||
// targetSdkVersion flutter.targetSdkVersion
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (project.hasProperty("keyStoreFile")) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
// For testing purposes we sign with dummy credentials if no key properties are given.
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.google.android.gms:play-services-auth:20.6.0'
|
||||
}
|
||||
73
app/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,73 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.feeddeck.feeddeck"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
// Use 27.0.12077973 as NDK version instead of the default which is defined in ~/flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt
|
||||
// ndkVersion = flutter.ndkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "app.feeddeck.feeddeck"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.google.android.gms:play-services-auth:20.6.0")
|
||||
// This is required for the "file_picker" Flutter package, because without the app crash when exporting a file with the following error:
|
||||
// "java.lang.NoClassDefFoundError: Failed resolution of: Ljavax/xml/stream/XMLResolver;"
|
||||
implementation("javax.xml.stream:stax-api:1.0-2")
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
@@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
21
app/android/build.gradle.kts
Normal file
@@ -0,0 +1,21 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
@@ -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.12-all.zip
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
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"
|
||||
25
app/android/settings.gradle.kts
Normal file
@@ -0,0 +1,25 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath = run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.7.3" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" 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, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -27,6 +27,9 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
Pod::PICKER_MEDIA = false
|
||||
Pod::PICKER_AUDIO = false
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
@@ -1,149 +1,64 @@
|
||||
PODS:
|
||||
- app_links (0.0.1):
|
||||
- Flutter
|
||||
- audio_service (0.0.1):
|
||||
- Flutter
|
||||
- audio_session (0.0.1):
|
||||
- Flutter
|
||||
- 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):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- purchases_flutter (9.1.0):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- purchases_flutter (6.4.0):
|
||||
- Flutter
|
||||
- PurchasesHybridCommon (= 8.0.0)
|
||||
- PurchasesHybridCommon (8.0.0):
|
||||
- RevenueCat (= 4.30.5)
|
||||
- RevenueCat (4.30.5)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PurchasesHybridCommon (= 16.0.2)
|
||||
- PurchasesHybridCommon (16.0.2):
|
||||
- RevenueCat (= 5.33.1)
|
||||
- RevenueCat (5.33.1)
|
||||
- sign_in_with_apple (0.0.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- just_audio (from `.symlinks/plugins/just_audio/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- PurchasesHybridCommon
|
||||
- RevenueCat
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
audio_service:
|
||||
:path: ".symlinks/plugins/audio_service/ios"
|
||||
audio_session:
|
||||
:path: ".symlinks/plugins/audio_session/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
just_audio:
|
||||
:path: ".symlinks/plugins/just_audio/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
||||
media_kit_video:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
purchases_flutter:
|
||||
:path: ".symlinks/plugins/purchases_flutter/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sign_in_with_apple:
|
||||
:path: ".symlinks/plugins/sign_in_with_apple/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
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
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
purchases_flutter: 818eabac676b9037ac7692d4bc48a52a10b276be
|
||||
PurchasesHybridCommon: ae7a0a6e105ecdde3e8816a004e57f0a2a7b9261
|
||||
RevenueCat: b0ed01125b05a45b8264a2951ad68acb61942038
|
||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
|
||||
wakelock_plus: fd58c82b1388f4afe3fe8aa2c856503a262a5b03
|
||||
|
||||
PODFILE CHECKSUM: ec83c31511fbc978a9918c6fda235238118483f5
|
||||
PODFILE CHECKSUM: a35dde46ea09af570b675187a949f5fa3bc82280
|
||||
|
||||
COCOAPODS: 1.14.2
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -57,6 +58,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
11A5CB70E48EE53811F041B1 /* Pods_Runner.framework in Frameworks */,
|
||||
55F35B592ABF74D1007331B3 /* StoreKit.framework in Frameworks */,
|
||||
);
|
||||
@@ -134,6 +136,9 @@
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
@@ -159,9 +164,12 @@
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@@ -348,7 +356,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 +436,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 +485,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;
|
||||
@@ -561,6 +569,18 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"originHash" : "34b4b4fd64d12b3f877c43c5c1afbd609747cf0fa590d3e8fb35fc7cd91a1fa3",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkimagepickercontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||
"state" : {
|
||||
"branch" : "4.3.9",
|
||||
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkphotogallery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "b62cb63bf4ed1f04c961a56c9c6c9d5ab8524ec6",
|
||||
"version" : "5.21.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||
"state" : {
|
||||
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||
"version" : "5.4.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||
"state" : {
|
||||
"revision" : "a634cb7cdfd580006e79a6e74e64417fe9e9783b",
|
||||
"version" : "2.7.4"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -1,10 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -26,6 +44,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
@@ -43,11 +62,13 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"originHash" : "34b4b4fd64d12b3f877c43c5c1afbd609747cf0fa590d3e8fb35fc7cd91a1fa3",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkimagepickercontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||
"state" : {
|
||||
"branch" : "4.3.9",
|
||||
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkphotogallery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "b62cb63bf4ed1f04c961a56c9c6c9d5ab8524ec6",
|
||||
"version" : "5.21.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||
"state" : {
|
||||
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||
"version" : "5.4.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||
"state" : {
|
||||
"revision" : "a634cb7cdfd580006e79a6e74e64417fe9e9783b",
|
||||
"version" : "2.7.4"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
|
||||
@UIApplicationMain
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,73 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>FeedDeck</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>feeddeck</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>app.feeddeck.feeddeck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>FeedDeck</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>feeddeck</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>app.feeddeck.feeddeck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:feeddeck/repositories/settings_repository.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/confirmation/confirmation.dart';
|
||||
import 'package:feeddeck/widgets/home/home.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_audio_palyer/item_audio_player_init/item_audio_player_init.dart';
|
||||
import 'package:feeddeck/widgets/reset_password/reset_password.dart';
|
||||
|
||||
/// Before we are calling [runApp] we have to ensure that the widget bindings
|
||||
@@ -50,6 +51,9 @@ void main() async {
|
||||
/// Initialize the [media_kit] packages, so that we can play audio and video
|
||||
/// files.
|
||||
MediaKit.ensureInitialized();
|
||||
if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
|
||||
ItemAudioPlayerInit().init();
|
||||
}
|
||||
|
||||
/// Initialize the [just_audio_background] package, so that we can play audio
|
||||
/// files in the background.
|
||||
@@ -57,7 +61,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',
|
||||
@@ -87,12 +91,12 @@ void main() async {
|
||||
class FeedDeckScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices => {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.unknown,
|
||||
};
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.unknown,
|
||||
};
|
||||
}
|
||||
|
||||
/// [onGenerateRoute] is used in `onGenerateRoute` and `onGenerateInitialRoutes`
|
||||
@@ -115,15 +119,11 @@ Route onGenerateRoute(RouteSettings settings) {
|
||||
),
|
||||
);
|
||||
case '/reset-password':
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const ResetPassword(),
|
||||
);
|
||||
return MaterialPageRoute(builder: (_) => const ResetPassword());
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const Home(),
|
||||
);
|
||||
return MaterialPageRoute(builder: (_) => const Home());
|
||||
}
|
||||
|
||||
/// The [FeedDeckApp] is the root widget of the app. The widget is used to
|
||||
@@ -155,8 +155,6 @@ class FeedDeckApp extends StatelessWidget {
|
||||
onSecondary: Constants.onSecondary,
|
||||
error: Constants.error,
|
||||
onError: Constants.onError,
|
||||
background: Constants.background,
|
||||
onBackground: Constants.onBackground,
|
||||
surface: Constants.surface,
|
||||
onSurface: Constants.onSurface,
|
||||
),
|
||||
@@ -168,32 +166,26 @@ class FeedDeckApp extends StatelessWidget {
|
||||
elevation: Constants.appBarElevation,
|
||||
),
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
backgroundColor: Constants.surface,
|
||||
contentTextStyle: TextStyle(
|
||||
color: Constants.onSurface,
|
||||
),
|
||||
backgroundColor: Constants.secondary,
|
||||
contentTextStyle: TextStyle(color: Constants.onSurface),
|
||||
),
|
||||
dialogTheme: const DialogTheme(
|
||||
backgroundColor: Constants.background,
|
||||
surfaceTintColor: Constants.background,
|
||||
contentTextStyle: TextStyle(
|
||||
color: Constants.onBackground,
|
||||
),
|
||||
dialogTheme: const DialogThemeData(
|
||||
backgroundColor: Constants.surface,
|
||||
surfaceTintColor: Constants.surface,
|
||||
contentTextStyle: TextStyle(color: Constants.onSurface),
|
||||
),
|
||||
popupMenuTheme: const PopupMenuThemeData(
|
||||
color: Constants.background,
|
||||
surfaceTintColor: Constants.background,
|
||||
textStyle: TextStyle(
|
||||
color: Constants.onBackground,
|
||||
),
|
||||
color: Constants.surface,
|
||||
surfaceTintColor: Constants.surface,
|
||||
textStyle: TextStyle(color: Constants.onSurface),
|
||||
),
|
||||
drawerTheme: const DrawerThemeData(
|
||||
backgroundColor: Constants.background,
|
||||
surfaceTintColor: Constants.background,
|
||||
backgroundColor: Constants.surface,
|
||||
surfaceTintColor: Constants.surface,
|
||||
),
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
backgroundColor: Constants.background,
|
||||
surfaceTintColor: Constants.background,
|
||||
backgroundColor: Constants.surface,
|
||||
surfaceTintColor: Constants.surface,
|
||||
),
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
@@ -206,8 +198,9 @@ class FeedDeckApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
scrollBehavior: FeedDeckScrollBehavior(),
|
||||
onGenerateInitialRoutes: (initialRoute) =>
|
||||
[onGenerateRoute(RouteSettings(name: initialRoute))],
|
||||
onGenerateInitialRoutes: (initialRoute) => [
|
||||
onGenerateRoute(RouteSettings(name: initialRoute)),
|
||||
],
|
||||
onGenerateRoute: (RouteSettings settings) =>
|
||||
onGenerateRoute(settings),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
|
||||
/// [FDColumn] is the model for a column in our app. The following fields are
|
||||
@@ -26,11 +28,12 @@ class FDColumn {
|
||||
id: data['id'],
|
||||
name: data['name'],
|
||||
position: data['position'],
|
||||
sources: data.containsKey('sources') && data['sources'] != null
|
||||
? List<FDSource>.from(
|
||||
data['sources'].map((source) => FDSource.fromJson(source)),
|
||||
)
|
||||
: [],
|
||||
sources:
|
||||
data.containsKey('sources') && data['sources'] != null
|
||||
? List<FDSource>.from(
|
||||
data['sources'].map((source) => FDSource.fromJson(source)),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,4 +43,34 @@ class FDColumn {
|
||||
String identifier() {
|
||||
return 'id: $id, sources: ${sources.map((source) => source.id).join(' ')}';
|
||||
}
|
||||
|
||||
factory FDColumn.fromXml(XmlElement element) {
|
||||
final sources = <FDSource>[];
|
||||
|
||||
element.findElements('outline').forEach((outline) {
|
||||
final source = FDSource.fromXml(outline);
|
||||
if (source.type != FDSourceType.none) {
|
||||
sources.add(source);
|
||||
}
|
||||
});
|
||||
|
||||
return FDColumn(
|
||||
id: '',
|
||||
name: element.getAttribute('text') ?? 'Unknown',
|
||||
position: 0,
|
||||
sources: sources,
|
||||
);
|
||||
}
|
||||
|
||||
void toXml(XmlBuilder builder) {
|
||||
builder.element(
|
||||
'outline',
|
||||
nest: () {
|
||||
builder.attribute('text', name);
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
sources[i].toXml(builder);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import 'package:feeddeck/models/sources/github.dart';
|
||||
import 'package:feeddeck/models/sources/googlenews.dart';
|
||||
import 'package:feeddeck/models/sources/stackoverflow.dart';
|
||||
@@ -8,8 +10,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 +23,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 +30,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 +43,6 @@ enum FDSourceType {
|
||||
rss,
|
||||
stackoverflow,
|
||||
tumblr,
|
||||
// x,
|
||||
youtube,
|
||||
none,
|
||||
}
|
||||
@@ -55,10 +59,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 +85,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 +95,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 +121,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 +131,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 +157,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 +168,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 +194,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:
|
||||
@@ -226,20 +238,25 @@ class FDSource {
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
String get prettyTitle => title.replaceAll(RegExp(r'(?! )\s+| \s+'), ' ');
|
||||
|
||||
factory FDSource.fromJson(Map<String, dynamic> data) {
|
||||
return FDSource(
|
||||
id: data['id'],
|
||||
type: getSourceTypeFromString(data['type']),
|
||||
title: data['title'],
|
||||
options: data.containsKey('options') && data['options'] != null
|
||||
? FDSourceOptions.fromJson(data['options'])
|
||||
: null,
|
||||
link: data.containsKey('link') && data['link'] != null
|
||||
? data['link']
|
||||
: null,
|
||||
icon: data.containsKey('icon') && data['icon'] != null
|
||||
? data['icon']
|
||||
: null,
|
||||
options:
|
||||
data.containsKey('options') && data['options'] != null
|
||||
? FDSourceOptions.fromJson(data['options'])
|
||||
: null,
|
||||
link:
|
||||
data.containsKey('link') && data['link'] != null
|
||||
? data['link']
|
||||
: null,
|
||||
icon:
|
||||
data.containsKey('icon') && data['icon'] != null
|
||||
? data['icon']
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,13 +270,55 @@ class FDSource {
|
||||
'icon': icon,
|
||||
};
|
||||
}
|
||||
|
||||
factory FDSource.fromXml(XmlElement element) {
|
||||
final type = getSourceTypeFromString(element.getAttribute('type') ?? '');
|
||||
final text = element.getAttribute('text');
|
||||
|
||||
if (type == FDSourceType.none || text == null) {
|
||||
return FDSource(
|
||||
id: '',
|
||||
type: FDSourceType.none,
|
||||
title: '',
|
||||
options: null,
|
||||
link: null,
|
||||
icon: null,
|
||||
);
|
||||
}
|
||||
|
||||
return FDSource(
|
||||
id: '',
|
||||
type: type,
|
||||
title: text,
|
||||
options: FDSourceOptions.fromXml(element),
|
||||
link: null,
|
||||
icon: null,
|
||||
);
|
||||
}
|
||||
|
||||
void toXml(XmlBuilder builder) {
|
||||
builder.element(
|
||||
'outline',
|
||||
nest: () {
|
||||
builder.attribute('type', type.toShortString());
|
||||
builder.attribute('text', prettyTitle);
|
||||
builder.attribute('htmlUrl', link);
|
||||
|
||||
if (options != null) {
|
||||
options!.toXml(builder);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [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 +328,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,24 +344,34 @@ 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'])
|
||||
: null,
|
||||
googlenews: responseData.containsKey('googlenews') &&
|
||||
responseData['googlenews'] != null
|
||||
? FDGoogleNewsOptions.fromJson(responseData['googlenews'])
|
||||
: null,
|
||||
mastodon: responseData.containsKey('mastodon') &&
|
||||
responseData['mastodon'] != null
|
||||
? responseData['mastodon']
|
||||
: null,
|
||||
googlenews:
|
||||
responseData.containsKey('googlenews') &&
|
||||
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']
|
||||
: null,
|
||||
medium:
|
||||
responseData.containsKey('medium') && responseData['medium'] != null
|
||||
? responseData['medium']
|
||||
@@ -310,10 +380,11 @@ class FDSourceOptions {
|
||||
responseData.containsKey('nitter') && responseData['nitter'] != null
|
||||
? responseData['nitter']
|
||||
: null,
|
||||
pinterest: responseData.containsKey('pinterest') &&
|
||||
responseData['pinterest'] != null
|
||||
? responseData['pinterest']
|
||||
: null,
|
||||
pinterest:
|
||||
responseData.containsKey('pinterest') &&
|
||||
responseData['pinterest'] != null
|
||||
? responseData['pinterest']
|
||||
: null,
|
||||
podcast:
|
||||
responseData.containsKey('podcast') && responseData['podcast'] != null
|
||||
? responseData['podcast']
|
||||
@@ -322,20 +393,19 @@ class FDSourceOptions {
|
||||
responseData.containsKey('reddit') && responseData['reddit'] != null
|
||||
? responseData['reddit']
|
||||
: null,
|
||||
rss: responseData.containsKey('rss') && responseData['rss'] != null
|
||||
? responseData['rss']
|
||||
: null,
|
||||
stackoverflow: responseData.containsKey('stackoverflow') &&
|
||||
responseData['stackoverflow'] != null
|
||||
? FDStackOverflowOptions.fromJson(responseData['stackoverflow'])
|
||||
: null,
|
||||
rss:
|
||||
responseData.containsKey('rss') && responseData['rss'] != null
|
||||
? responseData['rss']
|
||||
: null,
|
||||
stackoverflow:
|
||||
responseData.containsKey('stackoverflow') &&
|
||||
responseData['stackoverflow'] != null
|
||||
? FDStackOverflowOptions.fromJson(responseData['stackoverflow'])
|
||||
: null,
|
||||
tumblr:
|
||||
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 +415,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,8 +428,58 @@ class FDSourceOptions {
|
||||
'rss': rss,
|
||||
'stackoverflow': stackoverflow?.toJson(),
|
||||
'tumblr': tumblr,
|
||||
'x': x,
|
||||
'youtube': youtube,
|
||||
};
|
||||
}
|
||||
|
||||
factory FDSourceOptions.fromXml(XmlElement element) {
|
||||
return FDSourceOptions(
|
||||
fourchan: element.getAttribute('fourchan'),
|
||||
github: FDGitHubOptions.fromXml(element),
|
||||
googlenews: FDGoogleNewsOptions.fromXml(element),
|
||||
lemmy: element.getAttribute('lemmy'),
|
||||
mastodon: element.getAttribute('mastodon'),
|
||||
medium: element.getAttribute('medium'),
|
||||
nitter: element.getAttribute('nitter'),
|
||||
pinterest: element.getAttribute('pinterest'),
|
||||
podcast: element.getAttribute('podcast'),
|
||||
reddit: element.getAttribute('reddit'),
|
||||
rss: element.getAttribute('xmlUrl'),
|
||||
stackoverflow: FDStackOverflowOptions.fromXml(element),
|
||||
tumblr: element.getAttribute('tumblr'),
|
||||
youtube: element.getAttribute('youtube'),
|
||||
);
|
||||
}
|
||||
|
||||
void toXml(XmlBuilder builder) {
|
||||
if (fourchan != null) {
|
||||
builder.attribute('fourchan', fourchan);
|
||||
} else if (github != null) {
|
||||
github!.toXml(builder);
|
||||
} else if (googlenews != null) {
|
||||
googlenews!.toXml(builder);
|
||||
} else if (lemmy != null) {
|
||||
builder.attribute('lemmy', lemmy);
|
||||
} else if (mastodon != null) {
|
||||
builder.attribute('mastodon', mastodon);
|
||||
} else if (medium != null) {
|
||||
builder.attribute('medium', medium);
|
||||
} else if (nitter != null) {
|
||||
builder.attribute('nitter', nitter);
|
||||
} else if (pinterest != null) {
|
||||
builder.attribute('pinterest', pinterest);
|
||||
} else if (podcast != null) {
|
||||
builder.attribute('podcast', podcast);
|
||||
} else if (reddit != null) {
|
||||
builder.attribute('reddit', reddit);
|
||||
} else if (rss != null) {
|
||||
builder.attribute('xmlUrl', rss);
|
||||
} else if (stackoverflow != null) {
|
||||
stackoverflow!.toXml(builder);
|
||||
} else if (tumblr != null) {
|
||||
builder.attribute('tumblr', tumblr);
|
||||
} else if (youtube != null) {
|
||||
builder.attribute('youtube', youtube);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
/// [FDGitHubType] is an enum value which defines the type for the GitHub
|
||||
/// source.
|
||||
enum FDGitHubType {
|
||||
@@ -88,31 +90,38 @@ class FDGitHubOptions {
|
||||
|
||||
factory FDGitHubOptions.fromJson(Map<String, dynamic> responseData) {
|
||||
return FDGitHubOptions(
|
||||
type: responseData.containsKey('type') && responseData['type'] != null
|
||||
? getGitHubTypeFromString(responseData['type'])
|
||||
: null,
|
||||
participating: responseData.containsKey('participating') &&
|
||||
responseData['participating'] != null
|
||||
? responseData['participating']
|
||||
: null,
|
||||
repository: responseData.containsKey('repository') &&
|
||||
responseData['repository'] != null
|
||||
? responseData['repository']
|
||||
: null,
|
||||
user: responseData.containsKey('user') && responseData['user'] != null
|
||||
? responseData['user']
|
||||
: null,
|
||||
organization: responseData.containsKey('organization') &&
|
||||
responseData['organization'] != null
|
||||
? responseData['organization']
|
||||
: null,
|
||||
queryName: responseData.containsKey('queryName') &&
|
||||
responseData['queryName'] != null
|
||||
? responseData['queryName']
|
||||
: null,
|
||||
query: responseData.containsKey('query') && responseData['query'] != null
|
||||
? responseData['query']
|
||||
: null,
|
||||
type:
|
||||
responseData.containsKey('type') && responseData['type'] != null
|
||||
? getGitHubTypeFromString(responseData['type'])
|
||||
: null,
|
||||
participating:
|
||||
responseData.containsKey('participating') &&
|
||||
responseData['participating'] != null
|
||||
? responseData['participating']
|
||||
: null,
|
||||
repository:
|
||||
responseData.containsKey('repository') &&
|
||||
responseData['repository'] != null
|
||||
? responseData['repository']
|
||||
: null,
|
||||
user:
|
||||
responseData.containsKey('user') && responseData['user'] != null
|
||||
? responseData['user']
|
||||
: null,
|
||||
organization:
|
||||
responseData.containsKey('organization') &&
|
||||
responseData['organization'] != null
|
||||
? responseData['organization']
|
||||
: null,
|
||||
queryName:
|
||||
responseData.containsKey('queryName') &&
|
||||
responseData['queryName'] != null
|
||||
? responseData['queryName']
|
||||
: null,
|
||||
query:
|
||||
responseData.containsKey('query') && responseData['query'] != null
|
||||
? responseData['query']
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,4 +136,40 @@ class FDGitHubOptions {
|
||||
'query': query,
|
||||
};
|
||||
}
|
||||
|
||||
factory FDGitHubOptions.fromXml(XmlElement element) {
|
||||
return FDGitHubOptions(
|
||||
type: getGitHubTypeFromString(element.getAttribute('githubType') ?? ''),
|
||||
participating: element.getAttribute('githubParticipating') == 'true',
|
||||
repository: element.getAttribute('githubRepository'),
|
||||
user: element.getAttribute('githubUser'),
|
||||
organization: element.getAttribute('githubOrganization'),
|
||||
queryName: element.getAttribute('githubQueryName'),
|
||||
query: element.getAttribute('githubQuery'),
|
||||
);
|
||||
}
|
||||
|
||||
void toXml(XmlBuilder builder) {
|
||||
if (type != null) {
|
||||
builder.attribute('githubType', type!.toShortString());
|
||||
}
|
||||
if (participating != null) {
|
||||
builder.attribute('githubParticipating', participating);
|
||||
}
|
||||
if (repository != null) {
|
||||
builder.attribute('githubRepository', repository);
|
||||
}
|
||||
if (user != null) {
|
||||
builder.attribute('githubUser', user);
|
||||
}
|
||||
if (organization != null) {
|
||||
builder.attribute('githubOrganization', organization);
|
||||
}
|
||||
if (queryName != null) {
|
||||
builder.attribute('githubQueryName', queryName);
|
||||
}
|
||||
if (query != null) {
|
||||
builder.attribute('githubQuery', query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
/// [FDGoogleNewsType] is an enum value which defines the type for the Google
|
||||
/// News source.
|
||||
enum FDGoogleNewsType {
|
||||
url,
|
||||
search,
|
||||
}
|
||||
enum FDGoogleNewsType { url, search }
|
||||
|
||||
/// [FDGoogleNewsTypeExtension] defines all extensions which are available for
|
||||
/// the [FDGoogleNewsType] enum type.
|
||||
@@ -61,25 +60,30 @@ class FDGoogleNewsOptions {
|
||||
|
||||
factory FDGoogleNewsOptions.fromJson(Map<String, dynamic> responseData) {
|
||||
return FDGoogleNewsOptions(
|
||||
type: responseData.containsKey('type') && responseData['type'] != null
|
||||
? getGoogleNewsTypeFromString(responseData['type'])
|
||||
: null,
|
||||
url: responseData.containsKey('url') && responseData['url'] != null
|
||||
? responseData['url']
|
||||
: null,
|
||||
type:
|
||||
responseData.containsKey('type') && responseData['type'] != null
|
||||
? getGoogleNewsTypeFromString(responseData['type'])
|
||||
: null,
|
||||
url:
|
||||
responseData.containsKey('url') && responseData['url'] != null
|
||||
? responseData['url']
|
||||
: null,
|
||||
search:
|
||||
responseData.containsKey('search') && responseData['search'] != null
|
||||
? responseData['search']
|
||||
: null,
|
||||
ceid: responseData.containsKey('ceid') && responseData['ceid'] != null
|
||||
? responseData['ceid']
|
||||
: null,
|
||||
gl: responseData.containsKey('gl') && responseData['gl'] != null
|
||||
? responseData['gl']
|
||||
: null,
|
||||
hl: responseData.containsKey('hl') && responseData['hl'] != null
|
||||
? responseData['hl']
|
||||
: null,
|
||||
ceid:
|
||||
responseData.containsKey('ceid') && responseData['ceid'] != null
|
||||
? responseData['ceid']
|
||||
: null,
|
||||
gl:
|
||||
responseData.containsKey('gl') && responseData['gl'] != null
|
||||
? responseData['gl']
|
||||
: null,
|
||||
hl:
|
||||
responseData.containsKey('hl') && responseData['hl'] != null
|
||||
? responseData['hl']
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,4 +97,38 @@ class FDGoogleNewsOptions {
|
||||
'hl': hl,
|
||||
};
|
||||
}
|
||||
|
||||
factory FDGoogleNewsOptions.fromXml(XmlElement element) {
|
||||
return FDGoogleNewsOptions(
|
||||
type: getGoogleNewsTypeFromString(
|
||||
element.getAttribute('googlenewsType') ?? '',
|
||||
),
|
||||
url: element.getAttribute('googlenewsUrl'),
|
||||
search: element.getAttribute('googlenewsSearch'),
|
||||
ceid: element.getAttribute('googlenewsCeid'),
|
||||
gl: element.getAttribute('googlenewsGl'),
|
||||
hl: element.getAttribute('googlenewsHl'),
|
||||
);
|
||||
}
|
||||
|
||||
void toXml(XmlBuilder builder) {
|
||||
if (type != null) {
|
||||
builder.attribute('googlenewsType', type!.toShortString());
|
||||
}
|
||||
if (url != null) {
|
||||
builder.attribute('googlenewsUrl', url);
|
||||
}
|
||||
if (search != null) {
|
||||
builder.attribute('googlenewsSearch', search);
|
||||
}
|
||||
if (ceid != null) {
|
||||
builder.attribute('googlenewsCeid', ceid);
|
||||
}
|
||||
if (gl != null) {
|
||||
builder.attribute('googlenewsGl', gl);
|
||||
}
|
||||
if (hl != null) {
|
||||
builder.attribute('googlenewsHl', hl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
/// [FDStackOverflowType] is an enum value which defines the type for the
|
||||
/// StackOverflow source.
|
||||
enum FDStackOverflowType {
|
||||
url,
|
||||
tag,
|
||||
}
|
||||
enum FDStackOverflowType { url, tag }
|
||||
|
||||
/// [FDStackOverflowTypeExtension] defines all extensions which are available for
|
||||
/// the [FDStackOverflowType] enum type.
|
||||
@@ -42,12 +41,7 @@ FDStackOverflowType getStackOverflowTypeFromString(String state) {
|
||||
|
||||
/// [FDStackOverflowSort] is an enum value which defines the available sort
|
||||
/// properties for the [FDStackOverflowOptions]
|
||||
enum FDStackOverflowSort {
|
||||
newest,
|
||||
active,
|
||||
featured,
|
||||
votes,
|
||||
}
|
||||
enum FDStackOverflowSort { newest, active, featured, votes }
|
||||
|
||||
/// [FDStackOverflowSortExtension] defines all extensions which are available
|
||||
/// for the [FDStackOverflowSort] enum type.
|
||||
@@ -100,27 +94,26 @@ class FDStackOverflowOptions {
|
||||
String? tag;
|
||||
FDStackOverflowSort? sort;
|
||||
|
||||
FDStackOverflowOptions({
|
||||
this.type,
|
||||
this.url,
|
||||
this.tag,
|
||||
this.sort,
|
||||
});
|
||||
FDStackOverflowOptions({this.type, this.url, this.tag, this.sort});
|
||||
|
||||
factory FDStackOverflowOptions.fromJson(Map<String, dynamic> responseData) {
|
||||
return FDStackOverflowOptions(
|
||||
type: responseData.containsKey('type') && responseData['type'] != null
|
||||
? getStackOverflowTypeFromString(responseData['type'])
|
||||
: null,
|
||||
url: responseData.containsKey('url') && responseData['url'] != null
|
||||
? responseData['url']
|
||||
: null,
|
||||
tag: responseData.containsKey('tag') && responseData['tag'] != null
|
||||
? responseData['tag']
|
||||
: null,
|
||||
sort: responseData.containsKey('sort') && responseData['sort'] != null
|
||||
? getStackOverflowSortFromString(responseData['sort'])
|
||||
: null,
|
||||
type:
|
||||
responseData.containsKey('type') && responseData['type'] != null
|
||||
? getStackOverflowTypeFromString(responseData['type'])
|
||||
: null,
|
||||
url:
|
||||
responseData.containsKey('url') && responseData['url'] != null
|
||||
? responseData['url']
|
||||
: null,
|
||||
tag:
|
||||
responseData.containsKey('tag') && responseData['tag'] != null
|
||||
? responseData['tag']
|
||||
: null,
|
||||
sort:
|
||||
responseData.containsKey('sort') && responseData['sort'] != null
|
||||
? getStackOverflowSortFromString(responseData['sort'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,4 +125,32 @@ class FDStackOverflowOptions {
|
||||
'sort': sort?.toShortString(),
|
||||
};
|
||||
}
|
||||
|
||||
factory FDStackOverflowOptions.fromXml(XmlElement element) {
|
||||
return FDStackOverflowOptions(
|
||||
type: getStackOverflowTypeFromString(
|
||||
element.getAttribute('stackoverflowType') ?? '',
|
||||
),
|
||||
url: element.getAttribute('stackoverflowUrl'),
|
||||
tag: element.getAttribute('stackoverflowTag'),
|
||||
sort: getStackOverflowSortFromString(
|
||||
element.getAttribute('stackoverflowSort') ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void toXml(XmlBuilder builder) {
|
||||
if (type != null) {
|
||||
builder.attribute('stackoverflowType', type!.toShortString());
|
||||
}
|
||||
if (url != null) {
|
||||
builder.attribute('stackoverflowUrl', url);
|
||||
}
|
||||
if (tag != null) {
|
||||
builder.attribute('stackoverflowTag', tag);
|
||||
}
|
||||
if (sort != null) {
|
||||
builder.attribute('stackoverflowSort', sort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,7 @@ import 'package:feeddeck/models/deck.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/api_exception.dart';
|
||||
|
||||
enum FDAppStatus {
|
||||
uninitialized,
|
||||
authenticated,
|
||||
unauthenticated,
|
||||
}
|
||||
enum FDAppStatus { uninitialized, authenticated, unauthenticated }
|
||||
|
||||
/// [AppRepository] is the repository for our app. The repository is responsible
|
||||
/// for managing the state of our app, this includes the authentication status
|
||||
@@ -82,10 +78,7 @@ class AppRepository with ChangeNotifier {
|
||||
///
|
||||
/// If the user was signed in successfully, we run the same logic as in the
|
||||
/// [init] function, to set the active deck for the user.
|
||||
Future<void> signInWithPassword(
|
||||
String email,
|
||||
String password,
|
||||
) async {
|
||||
Future<void> signInWithPassword(String email, String password) async {
|
||||
await Supabase.instance.client.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
@@ -115,9 +108,7 @@ class AppRepository with ChangeNotifier {
|
||||
///
|
||||
/// If the user was signed in successfully, we run the same logic as in the
|
||||
/// [init] function, to set the active deck for the user.
|
||||
Future<void> signInWithCallback(
|
||||
Uri uri,
|
||||
) async {
|
||||
Future<void> signInWithCallback(Uri uri) async {
|
||||
await Supabase.instance.client.auth.getSessionFromUrl(
|
||||
uri,
|
||||
storeSession: true,
|
||||
@@ -145,17 +136,16 @@ class AppRepository with ChangeNotifier {
|
||||
/// create a new deck for the user with the given name. After the deck was
|
||||
/// created, the deck is set as the users active deck and the deck is added to
|
||||
/// the list of decks.
|
||||
Future<void> createDeck(
|
||||
String name,
|
||||
) async {
|
||||
final data = await Supabase.instance.client
|
||||
.from('decks')
|
||||
.insert({
|
||||
'name': name,
|
||||
'userId': Supabase.instance.client.auth.currentUser!.id,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
Future<void> createDeck(String name) async {
|
||||
final data =
|
||||
await Supabase.instance.client
|
||||
.from('decks')
|
||||
.insert({
|
||||
'name': name,
|
||||
'userId': Supabase.instance.client.auth.currentUser!.id,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
final newDeck = FDDeck.fromJson(data);
|
||||
|
||||
@@ -178,17 +168,28 @@ class AppRepository with ChangeNotifier {
|
||||
return List<FDDeck>.from(data.map((deck) => FDDeck.fromJson(deck)));
|
||||
}
|
||||
|
||||
/// [getDecksWithNotifiy] uses the [getDecks] function to get a list of all
|
||||
/// decks for the user, but instead of returning the decks it updates the
|
||||
/// [_decks] property and calls all the registered listeners.
|
||||
///
|
||||
/// This function can be used to trigger an update of the decks when they are
|
||||
/// created outside of the AppRepository, like it is done in the import
|
||||
/// process.
|
||||
Future<void> getDecksWithNotifiy() async {
|
||||
final decks = await getDecks();
|
||||
_decks = decks;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// [updateDeck] is called to update a deck for the user. The function takes
|
||||
/// a [deckId] and a [name] as parameters. The function calls the Supabase
|
||||
/// client to update the name of the deck. After the deck was updated, the
|
||||
/// deck is also updated in the list of decks.
|
||||
Future<void> updateDeck(
|
||||
String deckId,
|
||||
String name,
|
||||
) async {
|
||||
Future<void> updateDeck(String deckId, String name) async {
|
||||
await Supabase.instance.client
|
||||
.from('decks')
|
||||
.update({'name': name}).eq('id', deckId);
|
||||
.update({'name': name})
|
||||
.eq('id', deckId);
|
||||
|
||||
for (var i = 0; i < _decks.length; i++) {
|
||||
if (_decks[i].id == deckId) {
|
||||
@@ -205,9 +206,7 @@ class AppRepository with ChangeNotifier {
|
||||
/// the deck. After the deck was deleted, the deck is also removed from the
|
||||
/// list of decks. If the deleted deck was the active deck, the active deck is
|
||||
/// set to `null`.
|
||||
Future<void> deleteDeck(
|
||||
String deckId,
|
||||
) async {
|
||||
Future<void> deleteDeck(String deckId) async {
|
||||
await Supabase.instance.client.from('decks').delete().eq('id', deckId);
|
||||
|
||||
_decks.removeWhere((deck) => deck.id == deckId);
|
||||
@@ -223,9 +222,7 @@ class AppRepository with ChangeNotifier {
|
||||
/// to get the columns for the deck and all sources for each column. After the
|
||||
/// columns and sources are fetched, the active deck is set to the provided
|
||||
/// deckId and the columns and sources are stored in the repository.
|
||||
Future<void> selectDeck(
|
||||
String deckId,
|
||||
) async {
|
||||
Future<void> selectDeck(String deckId) async {
|
||||
final columns = await getColumns(deckId);
|
||||
for (final column in columns) {
|
||||
column.sources = await getSources(column.id);
|
||||
@@ -243,18 +240,18 @@ class AppRepository with ChangeNotifier {
|
||||
/// function takes a [name] as parameter. The function calls the Supabase
|
||||
/// client to create a new column for the active deck with the given name.
|
||||
/// Finally the newly created column is added to the list of columns.
|
||||
Future<void> createColumn(
|
||||
String name,
|
||||
) async {
|
||||
final data = await Supabase.instance.client.from('columns').insert({
|
||||
'deckId': _activeDeckId,
|
||||
'userId': Supabase.instance.client.auth.currentUser!.id,
|
||||
'name': name,
|
||||
'position': _columns.length,
|
||||
}).select();
|
||||
Future<void> createColumn(String name) async {
|
||||
final data =
|
||||
await Supabase.instance.client.from('columns').insert({
|
||||
'deckId': _activeDeckId,
|
||||
'userId': Supabase.instance.client.auth.currentUser!.id,
|
||||
'name': name,
|
||||
'position': _columns.length,
|
||||
}).select();
|
||||
|
||||
final newColumn =
|
||||
List<FDColumn>.from(data.map((column) => FDColumn.fromJson(column)));
|
||||
final newColumn = List<FDColumn>.from(
|
||||
data.map((column) => FDColumn.fromJson(column)),
|
||||
);
|
||||
_columns.addAll(newColumn);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -262,9 +259,7 @@ class AppRepository with ChangeNotifier {
|
||||
/// [getColumns] is called to get all columns for the deck with the provided
|
||||
/// [deckId]. The function calls the Supabase client to get all columns for
|
||||
/// the deck. The function returns a list of [FDColumn]s.
|
||||
Future<List<FDColumn>> getColumns(
|
||||
String deckId,
|
||||
) async {
|
||||
Future<List<FDColumn>> getColumns(String deckId) async {
|
||||
final data = await Supabase.instance.client
|
||||
.from('columns')
|
||||
.select('id, name, position')
|
||||
@@ -276,9 +271,7 @@ class AppRepository with ChangeNotifier {
|
||||
/// [deleteColumn] is called to delete a column with the provided [columnId].
|
||||
/// The function calls the Supabase client to delete the column. After the
|
||||
/// column was deleted, the column is also removed from the list of columns.
|
||||
Future<void> deleteColumn(
|
||||
String columnId,
|
||||
) async {
|
||||
Future<void> deleteColumn(String columnId) async {
|
||||
await Supabase.instance.client.from('columns').delete().eq('id', columnId);
|
||||
|
||||
_columns.removeWhere((column) => column.id == columnId);
|
||||
@@ -289,13 +282,11 @@ class AppRepository with ChangeNotifier {
|
||||
/// The function takes a [name] as parameter. The function calls the Supabase
|
||||
/// client to update the name of the column. After the column was updated, the
|
||||
/// column is also updated in the list of columns.
|
||||
Future<void> updateColumn(
|
||||
String columnId,
|
||||
String name,
|
||||
) async {
|
||||
Future<void> updateColumn(String columnId, String name) async {
|
||||
await Supabase.instance.client
|
||||
.from('columns')
|
||||
.update({'name': name}).eq('id', columnId);
|
||||
.update({'name': name})
|
||||
.eq('id', columnId);
|
||||
|
||||
for (var i = 0; i < _columns.length; i++) {
|
||||
if (_columns[i].id == columnId) {
|
||||
@@ -311,22 +302,15 @@ class AppRepository with ChangeNotifier {
|
||||
/// with the provided [index1] and [index2]. The function calls the Supabase
|
||||
/// client to update the positions of the columns. After the columns were
|
||||
/// updated, the columns are also updated in the list of columns.
|
||||
Future<void> updateColumnPositions(
|
||||
int index1,
|
||||
int index2,
|
||||
) async {
|
||||
Future<void> updateColumnPositions(int index1, int index2) async {
|
||||
await Supabase.instance.client
|
||||
.from('columns')
|
||||
.update({'position': _columns[index2].position}).eq(
|
||||
'id',
|
||||
_columns[index1].id,
|
||||
);
|
||||
.update({'position': _columns[index2].position})
|
||||
.eq('id', _columns[index1].id);
|
||||
await Supabase.instance.client
|
||||
.from('columns')
|
||||
.update({'position': _columns[index1].position}).eq(
|
||||
'id',
|
||||
_columns[index2].id,
|
||||
);
|
||||
.update({'position': _columns[index1].position})
|
||||
.eq('id', _columns[index2].id);
|
||||
|
||||
final tmp = _columns[index1];
|
||||
_columns[index1] = _columns[index2];
|
||||
@@ -340,13 +324,19 @@ class AppRepository with ChangeNotifier {
|
||||
/// [getSources] is called to get all sources for the column with the provided
|
||||
/// [columnId]. The function calls the Supabase client to get all sources for
|
||||
/// the column. The function returns a list of [FDSource]s.
|
||||
Future<List<FDSource>> getSources(
|
||||
String columnId,
|
||||
) async {
|
||||
///
|
||||
/// The returned list of sources is ordered by the `position` field. Since the
|
||||
/// position field was added later there might be columns where the field is
|
||||
/// `null`, which will come after all columns with a `position`. The source
|
||||
/// where the position is `null` will be ordered by the `createdAt` date. This
|
||||
/// should retain the order as before the `position` field was added and
|
||||
/// should also work for new sources, which are added without a position.
|
||||
Future<List<FDSource>> getSources(String columnId) async {
|
||||
final data = await Supabase.instance.client
|
||||
.from('sources')
|
||||
.select('id, type, title, options, link, icon')
|
||||
.eq('columnId', columnId)
|
||||
.order('position', ascending: true, nullsFirst: false)
|
||||
.order('createdAt', ascending: true);
|
||||
return List<FDSource>.from(data.map((source) => FDSource.fromJson(source)));
|
||||
}
|
||||
@@ -355,10 +345,7 @@ class AppRepository with ChangeNotifier {
|
||||
/// The function calls the Supabase client to delete the source. After the
|
||||
/// source was deleted, the source is also removed from the list of sources of
|
||||
/// the column with the provided [columnId].
|
||||
Future<void> deleteSource(
|
||||
String columnId,
|
||||
String sourceId,
|
||||
) async {
|
||||
Future<void> deleteSource(String columnId, String sourceId) async {
|
||||
await Supabase.instance.client.from('sources').delete().eq('id', sourceId);
|
||||
|
||||
/// It could take some time before we can retrieve the items after a source
|
||||
@@ -376,21 +363,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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -411,4 +410,51 @@ class AppRepository with ChangeNotifier {
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// [updateSourcePositions] can be used to reorder the list of sources for a
|
||||
/// column. To achieve this order the list of sources for the provided
|
||||
/// [columnId] locally and update the `position` field of each source
|
||||
/// afterwards in the database.
|
||||
///
|
||||
/// We have to check if the user drags a source from top to bottom ([start]
|
||||
/// is lower then [current]) or from the bottom to the top ([start] is greater
|
||||
/// then [current]), to apply a different logic for the reordering.
|
||||
Future<void> updateSourcePositions(
|
||||
String columnId,
|
||||
int start,
|
||||
int current,
|
||||
) async {
|
||||
final columnIndex = _columns.indexWhere((column) => column.id == columnId);
|
||||
if (columnIndex == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (start < current) {
|
||||
int end = current - 1;
|
||||
FDSource startItem = _columns[columnIndex].sources[start];
|
||||
int i = 0;
|
||||
int local = start;
|
||||
do {
|
||||
_columns[columnIndex].sources[local] =
|
||||
_columns[columnIndex].sources[++local];
|
||||
i++;
|
||||
} while (i < end - start);
|
||||
_columns[columnIndex].sources[end] = startItem;
|
||||
} else if (start > current) {
|
||||
FDSource startItem = _columns[columnIndex].sources[start];
|
||||
for (int i = start; i > current; i--) {
|
||||
_columns[columnIndex].sources[i] = _columns[columnIndex].sources[i - 1];
|
||||
}
|
||||
_columns[columnIndex].sources[current] = startItem;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _columns[columnIndex].sources.length; i++) {
|
||||
await Supabase.instance.client
|
||||
.from('sources')
|
||||
.update({'position': i})
|
||||
.eq('id', _columns[columnIndex].sources[i].id);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,7 @@ class ItemsFilters {
|
||||
/// [ItemStateFilter] is a enum value which defines the state filter for items.
|
||||
/// The filter can be [read], [unread] or [bookmarked]. The [none] filter is
|
||||
/// used to return all items regardless if they are read, unread or bookmarked.
|
||||
enum ItemStateFilter {
|
||||
none,
|
||||
read,
|
||||
unread,
|
||||
bookmarked,
|
||||
}
|
||||
enum ItemStateFilter { none, read, unread, bookmarked }
|
||||
|
||||
/// The [ToString] extension defines a [toShortString] function, which returns a
|
||||
/// `String` which can be safely passed within a database query for the specifed
|
||||
@@ -53,11 +48,7 @@ extension ToString on ItemStateFilter {
|
||||
/// - [loading] during the time when the items are retrieved from our database
|
||||
/// - [loadedLast] when there are no more items which can be loaded from the
|
||||
/// database.
|
||||
enum ItemsStatus {
|
||||
loaded,
|
||||
loading,
|
||||
loadedLast,
|
||||
}
|
||||
enum ItemsStatus { loaded, loading, loadedLast }
|
||||
|
||||
/// [now] returns the current Unix timestamp in seconds.
|
||||
int now() {
|
||||
@@ -69,9 +60,7 @@ int now() {
|
||||
/// initialized we have to call the [_init] function to load the items for the
|
||||
/// provided column.
|
||||
class ItemsRepository with ChangeNotifier {
|
||||
ItemsRepository({
|
||||
required this.column,
|
||||
}) {
|
||||
ItemsRepository({required this.column}) {
|
||||
_init();
|
||||
}
|
||||
|
||||
@@ -141,7 +130,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);
|
||||
@@ -275,7 +264,8 @@ class ItemsRepository with ChangeNotifier {
|
||||
try {
|
||||
await Supabase.instance.client
|
||||
.from('items')
|
||||
.update({'isRead': read}).eq('id', itemId);
|
||||
.update({'isRead': read})
|
||||
.eq('id', itemId);
|
||||
for (var i = 0; i < _items.length; i++) {
|
||||
if (_items[i].id == itemId) {
|
||||
_items[i].isRead = read;
|
||||
@@ -305,7 +295,8 @@ 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;
|
||||
@@ -327,7 +318,8 @@ class ItemsRepository with ChangeNotifier {
|
||||
try {
|
||||
await Supabase.instance.client
|
||||
.from('items')
|
||||
.update({'isBookmarked': bookmarked}).eq('id', itemId);
|
||||
.update({'isBookmarked': bookmarked})
|
||||
.eq('id', itemId);
|
||||
for (var i = 0; i < _items.length; i++) {
|
||||
if (_items[i].id == itemId) {
|
||||
_items[i].isBookmarked = bookmarked;
|
||||
@@ -391,7 +383,7 @@ class ItemsRepositoryStore {
|
||||
///
|
||||
/// The best is to call the [set] function right before we call
|
||||
/// `notifyListeners` in the repository.
|
||||
set(
|
||||
ItemsRepositoryStoreState set(
|
||||
String columnId,
|
||||
ItemsStatus status,
|
||||
ItemsFilters filters,
|
||||
@@ -407,7 +399,7 @@ class ItemsRepositoryStore {
|
||||
/// [clear] deletes all the stored [_itemsRepositoryStoreStates] from the
|
||||
/// store. This method can be used to clear the cache, e.g. when a user
|
||||
/// changes the active deck or signes out.
|
||||
clear() {
|
||||
void clear() {
|
||||
_itemsRepositoryStoreStates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@ class Constants {
|
||||
static const primary = Color(0xff49d3b4);
|
||||
static const onPrimary = Color(0xff1f2229);
|
||||
static const secondary = Color(0xff353a46);
|
||||
static const onSecondary = Color(0xffe2e4e9);
|
||||
static const onSecondary = Color(0xff49d3b4);
|
||||
static const error = Color(0xffde4A40);
|
||||
static const onError = Color(0xffe2e4e9);
|
||||
static const background = Color(0xff1f2229);
|
||||
static const onBackground = Color(0xffe2e4e9);
|
||||
static const surface = Color(0xff353a46);
|
||||
static const surface = Color(0xff1f2229);
|
||||
static const onSurface = Color(0xffe2e4e9);
|
||||
static const canvasColor = Color(0xff1f2229);
|
||||
static const appBarBackgroundColor = Colors.transparent;
|
||||
@@ -23,7 +21,7 @@ class Constants {
|
||||
static const secondaryTextColor = Color(0xff9aa1b2);
|
||||
|
||||
static const dividerColor = Color(0xff2a2e38);
|
||||
static const backgroundContainerBackgroundColor = Color(0xff14161a);
|
||||
static const surfaceContainerBackgroundColor = Color(0xff14161a);
|
||||
|
||||
static const breakpoint = 600.0;
|
||||
static const columnWidth = 352.0;
|
||||
|
||||
@@ -56,15 +56,15 @@ const _htmlAuthFinished = '''
|
||||
</html>
|
||||
''';
|
||||
|
||||
/// The [DesktopLoginManager] is used to authenticate a user with the provided
|
||||
/// The [DesktopSignInManager] is used to authenticate a user with the provided
|
||||
/// OAuth [provider] on desktop platforms.
|
||||
class DesktopLoginManager {
|
||||
final supabase.Provider provider;
|
||||
class DesktopSignInManager {
|
||||
final supabase.OAuthProvider provider;
|
||||
final Map<String, String>? queryParams;
|
||||
|
||||
HttpServer? redirectServer;
|
||||
|
||||
DesktopLoginManager({
|
||||
DesktopSignInManager({
|
||||
required this.provider,
|
||||
required this.queryParams,
|
||||
});
|
||||
@@ -1,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;
|
||||
}
|
||||
@@ -4,24 +4,29 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// [openUrl] can be used to open the given [url] in the platforms default
|
||||
/// browser.
|
||||
/// [openUrl] can be used to open the given [url] in the specified launch mode.
|
||||
/// For iOS and Android we are using the In-App-Browser to launch the url, for
|
||||
/// all other platforms we are using the external browser.
|
||||
///
|
||||
/// On Android we are not using the default launch mode
|
||||
/// (`LaunchMode.platformDefault`), because the opened In-App-Browser is very
|
||||
/// limited, so that we decided to use `LaunchMode.externalApplication` to open
|
||||
/// the url.
|
||||
/// We do not have to check if the launch mode is really supported, because
|
||||
/// `launchUrl` will fallback to a supported launch mode, when our preferred
|
||||
/// mode is not supported.
|
||||
Future<void> openUrl(String url) async {
|
||||
var launchMode = LaunchMode.platformDefault;
|
||||
|
||||
if (!kIsWeb) {
|
||||
if (Platform.isAndroid) {
|
||||
launchMode = LaunchMode.externalApplication;
|
||||
}
|
||||
if (kIsWeb) {
|
||||
launchMode = LaunchMode.externalApplication;
|
||||
} else if (Platform.isAndroid) {
|
||||
launchMode = LaunchMode.inAppBrowserView;
|
||||
} else if (Platform.isIOS) {
|
||||
launchMode = LaunchMode.inAppBrowserView;
|
||||
} else if (Platform.isMacOS) {
|
||||
launchMode = LaunchMode.externalApplication;
|
||||
} else if (Platform.isLinux) {
|
||||
launchMode = LaunchMode.externalApplication;
|
||||
} else if (Platform.isWindows) {
|
||||
launchMode = LaunchMode.externalApplication;
|
||||
}
|
||||
|
||||
await launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: launchMode,
|
||||
);
|
||||
await launchUrl(Uri.parse(url), mode: launchMode);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -164,6 +164,8 @@ class _CreateColumnState extends State<CreateColumn> {
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -101,7 +101,7 @@ class _ColumnLayoutHeaderState extends State<ColumnLayoutHeader> {
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
backgroundColor: Constants.background,
|
||||
backgroundColor: Constants.surface,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -216,7 +216,7 @@ class _ColumnLayoutHeaderState extends State<ColumnLayoutHeader> {
|
||||
width: double.infinity,
|
||||
height: _showSettings,
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.backgroundContainerBackgroundColor,
|
||||
color: Constants.surfaceContainerBackgroundColor,
|
||||
),
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: ColumnLayoutHeaderSettings(
|
||||
|
||||
@@ -31,23 +31,22 @@ class _ColumnLayoutHeaderSettingsDeleteColumnState
|
||||
/// [_showDeleteDialog] creates a new dialog, which is shown before the column
|
||||
/// can be deleted. This is done to raise the awareness that the column,
|
||||
/// sources and items which belongs to the column will also be deleted.
|
||||
_showDeleteDialog() {
|
||||
void _showDeleteDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
insetPadding: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width >=
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width >=
|
||||
(Constants.centeredFormMaxWidth +
|
||||
2 * Constants.spacingMiddle)
|
||||
? (MediaQuery.of(context).size.width -
|
||||
Constants.centeredFormMaxWidth) /
|
||||
2
|
||||
Constants.centeredFormMaxWidth) /
|
||||
2
|
||||
: Constants.spacingMiddle,
|
||||
),
|
||||
title: const Text(
|
||||
'Delete Column',
|
||||
),
|
||||
title: const Text('Delete Column'),
|
||||
content: const Text(
|
||||
'Do you really want to delete this column? This can not be undone and will also delete all sources, items and bookmarks related to this column.',
|
||||
),
|
||||
@@ -88,8 +87,10 @@ class _ColumnLayoutHeaderSettingsDeleteColumnState
|
||||
});
|
||||
|
||||
try {
|
||||
await Provider.of<AppRepository>(context, listen: false)
|
||||
.deleteColumn(widget.column.id);
|
||||
await Provider.of<AppRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deleteColumn(widget.column.id);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -32,28 +32,66 @@ class ColumnLayoutHeaderSettingsSources extends StatefulWidget {
|
||||
|
||||
class _ColumnLayoutHeaderSettingsSourcesState
|
||||
extends State<ColumnLayoutHeaderSettingsSources> {
|
||||
/// [_proxyDecorator] is used to highlight the source which is currently
|
||||
/// draged by the user.
|
||||
Widget _proxyDecorator(Widget child, int index, Animation<double> animation) {
|
||||
return Material(
|
||||
elevation: 0,
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 16,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
elevation: 24,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// [_buildSourcesList] returns a list of all sources of the current column.
|
||||
/// If the list of sources is empty it will return a [Container].
|
||||
///
|
||||
/// Each source in the list also contains a delete item, which can be used to
|
||||
/// remove the source from the current column.
|
||||
List<Widget> _buildSourcesList() {
|
||||
Widget _buildSourcesList() {
|
||||
if (widget.column.sources.isEmpty) {
|
||||
return [Container()];
|
||||
return Container();
|
||||
}
|
||||
|
||||
List<Widget> columns = [];
|
||||
return ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
buildDefaultDragHandles: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onReorder: (int start, int current) {
|
||||
final AppRepository appRepository = Provider.of<AppRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
|
||||
for (var i = 0; i < widget.column.sources.length; i++) {
|
||||
columns.add(
|
||||
SourceListItem(
|
||||
appRepository.updateSourcePositions(widget.column.id, start, current);
|
||||
},
|
||||
proxyDecorator: (Widget child, int index, Animation<double> animation) {
|
||||
return _proxyDecorator(child, index, animation);
|
||||
},
|
||||
itemCount: widget.column.sources.length,
|
||||
itemBuilder: (context, index) {
|
||||
return SourceListItem(
|
||||
key: Key(widget.column.sources[index].id),
|
||||
columnId: widget.column.id,
|
||||
source: widget.column.sources[i],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return columns;
|
||||
sourceIndex: index,
|
||||
source: widget.column.sources[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// [_showAddSource] shows the [AddSource] widget within a modal bottom sheet
|
||||
@@ -92,16 +130,16 @@ class _ColumnLayoutHeaderSettingsSourcesState
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 275,
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 275),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
..._buildSourcesList(),
|
||||
_buildSourcesList(),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
@@ -114,9 +152,7 @@ class _ColumnLayoutHeaderSettingsSourcesState
|
||||
),
|
||||
label: const Text('Add Source'),
|
||||
onPressed: () => _showAddSource(),
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -101,7 +101,7 @@ class _ColumnLayoutSearchState extends State<ColumnLayoutSearch> {
|
||||
width: double.infinity,
|
||||
height: _showFilters,
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.backgroundContainerBackgroundColor,
|
||||
color: Constants.surfaceContainerBackgroundColor,
|
||||
),
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Column(
|
||||
|
||||
@@ -43,6 +43,8 @@ class _ConfirmationState extends State<Confirmation> {
|
||||
case 'change-email-address':
|
||||
return ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
@@ -57,6 +59,8 @@ class _ConfirmationState extends State<Confirmation> {
|
||||
case 'confirm-signup':
|
||||
return ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
@@ -71,6 +75,8 @@ class _ConfirmationState extends State<Confirmation> {
|
||||
case 'reset-password':
|
||||
return ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -179,6 +179,10 @@ class _CreateDeckState extends State<CreateDeck> {
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Name',
|
||||
hintText: 'e.g. News',
|
||||
hintStyle: TextStyle(
|
||||
color: Constants.secondaryTextColor,
|
||||
),
|
||||
),
|
||||
validator: (value) => _validateDeckName(value),
|
||||
onFieldSubmitted: (value) => _createDeck(),
|
||||
@@ -189,6 +193,8 @@ class _CreateDeckState extends State<CreateDeck> {
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -110,7 +110,7 @@ class _SelectDeckState extends State<SelectDeck> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Constants.background,
|
||||
color: Constants.surface,
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
|
||||
@@ -30,7 +30,7 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
|
||||
/// [_openDrawer] opens the provided [widget] in the drawer of the scaffold,
|
||||
/// by setting the [_drawer] state first and then opening the drawer.
|
||||
_openDrawer(Widget widget) {
|
||||
void _openDrawer(Widget widget) {
|
||||
setState(() {
|
||||
_drawer = widget;
|
||||
});
|
||||
@@ -63,20 +63,12 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
size: 32,
|
||||
),
|
||||
label: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: Constants.spacingExtraSmall,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 54,
|
||||
maxWidth: 54,
|
||||
),
|
||||
padding: const EdgeInsets.only(top: Constants.spacingExtraSmall),
|
||||
constraints: const BoxConstraints(minWidth: 54, maxWidth: 54),
|
||||
child: Text(
|
||||
Characters(column.name)
|
||||
.replaceAll(
|
||||
Characters(''),
|
||||
Characters('\u{200B}'),
|
||||
)
|
||||
.toString(),
|
||||
Characters(
|
||||
column.name,
|
||||
).replaceAll(Characters(''), Characters('\u{200B}')).toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
@@ -91,10 +83,7 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
for (var i = widgets.length; i < 2; i++) {
|
||||
widgets.add(
|
||||
const NavigationRailDestination(
|
||||
icon: Icon(
|
||||
Icons.circle,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
icon: Icon(Icons.circle, color: Colors.transparent),
|
||||
label: Text(''),
|
||||
),
|
||||
);
|
||||
@@ -119,20 +108,13 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: const TextSpan(
|
||||
style: TextStyle(
|
||||
color: Constants.onSurface,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
style: TextStyle(color: Constants.onSurface, fontSize: 14.0),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Add you first column by clicking on the plus icon (',
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Icon(Icons.add, size: 14.0),
|
||||
),
|
||||
TextSpan(
|
||||
text: ') in the sidebar on the left side.',
|
||||
),
|
||||
WidgetSpan(child: Icon(Icons.add, size: 14.0)),
|
||||
TextSpan(text: ') in the sidebar on the left side.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -200,10 +182,7 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
topRight: Radius.circular(Constants.spacingMiddle),
|
||||
bottomRight: Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
child: Drawer(
|
||||
width: Constants.columnWidth,
|
||||
child: _drawer,
|
||||
),
|
||||
child: Drawer(width: Constants.columnWidth, child: _drawer),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Row(
|
||||
@@ -211,7 +190,8 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: MediaQuery.of(context).size.height -
|
||||
minHeight:
|
||||
MediaQuery.of(context).size.height -
|
||||
MediaQuery.of(context).padding.top -
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
@@ -222,7 +202,7 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
),
|
||||
child: NavigationRail(
|
||||
backgroundColor: Constants.background,
|
||||
backgroundColor: Constants.surface,
|
||||
selectedIndex: null,
|
||||
|
||||
/// When a user selects a destination in the navigation
|
||||
@@ -281,9 +261,7 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingMiddle,
|
||||
),
|
||||
const SizedBox(height: Constants.spacingMiddle),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -29,16 +29,15 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
/// user before returning the index. If a column was deleted we reset the
|
||||
/// index to 0.
|
||||
int _getInitialIndex(BuildContext context, int columnsLength) {
|
||||
final deckLayoutSmallInitialTabIndex = Provider.of<LayoutRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deckLayoutSmallInitialTabIndex;
|
||||
final deckLayoutSmallInitialTabIndex =
|
||||
Provider.of<LayoutRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deckLayoutSmallInitialTabIndex;
|
||||
|
||||
if (deckLayoutSmallInitialTabIndex >= columnsLength) {
|
||||
Provider.of<LayoutRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deckLayoutSmallInitialTabIndex = 0;
|
||||
Provider.of<LayoutRepository>(context, listen: false)
|
||||
.deckLayoutSmallInitialTabIndex = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -61,25 +60,20 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
key: ValueKey(column.id),
|
||||
height: 56,
|
||||
icon: SourceIcon(
|
||||
type: column.sources.isNotEmpty
|
||||
? column.sources[0].type
|
||||
: FDSourceType.none,
|
||||
type:
|
||||
column.sources.isNotEmpty
|
||||
? column.sources[0].type
|
||||
: FDSourceType.none,
|
||||
icon: column.sources.isNotEmpty ? column.sources[0].icon : null,
|
||||
size: 24,
|
||||
),
|
||||
iconMargin: const EdgeInsets.only(bottom: 0),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 54,
|
||||
maxWidth: 54,
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 54, maxWidth: 54),
|
||||
child: Text(
|
||||
Characters(column.name)
|
||||
.replaceAll(
|
||||
Characters(''),
|
||||
Characters('\u{200B}'),
|
||||
)
|
||||
.toString(),
|
||||
Characters(
|
||||
column.name,
|
||||
).replaceAll(Characters(''), Characters('\u{200B}')).toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
@@ -106,20 +100,13 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: const TextSpan(
|
||||
style: TextStyle(
|
||||
color: Constants.onSurface,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
style: TextStyle(color: Constants.onSurface, fontSize: 14.0),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Add you first column by clicking on the plus icon (',
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Icon(Icons.add, size: 14.0),
|
||||
),
|
||||
TextSpan(
|
||||
text: ') in the tab bar on the bottom.',
|
||||
),
|
||||
WidgetSpan(child: Icon(Icons.add, size: 14.0)),
|
||||
TextSpan(text: ') in the tab bar on the bottom.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -138,8 +125,17 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Return the TabBarView. Since it was requested in
|
||||
/// https://github.com/feeddeck/feeddeck/issues/228 we removed the
|
||||
/// `physics: const NeverScrollableScrollPhysics()` property, so that a user
|
||||
/// can switch between tabs by swiping to the left and to the right.
|
||||
///
|
||||
/// After testing this didn't conflicted with the other scroll and swipe
|
||||
/// gestures of the children, so it should be save to activate. In case this
|
||||
/// doesn't workout well in the long term, we should re-add the `physics`
|
||||
/// property to the widget.
|
||||
return TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
children: widgets,
|
||||
);
|
||||
}
|
||||
@@ -149,6 +145,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(
|
||||
@@ -156,10 +153,7 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Constants.dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
top: BorderSide(color: Constants.dividerColor, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -169,21 +163,20 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: Theme.of(context).colorScheme.copyWith(
|
||||
surfaceVariant: Colors.transparent,
|
||||
),
|
||||
surfaceContainerHighest: Colors.transparent,
|
||||
),
|
||||
),
|
||||
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
|
||||
/// initial index when the widget is rebuild (e.g. when
|
||||
/// a user switches between the large and small layout).
|
||||
Provider.of<LayoutRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deckLayoutSmallInitialTabIndex = index;
|
||||
Provider.of<LayoutRepository>(context, listen: false)
|
||||
.deckLayoutSmallInitialTabIndex = index;
|
||||
},
|
||||
tabs: _buildTabs(context),
|
||||
),
|
||||
@@ -207,7 +200,7 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
color: Constants.onSecondary,
|
||||
color: Constants.onSurface,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
@@ -246,14 +239,15 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.settings,
|
||||
color: Constants.onSecondary,
|
||||
color: Constants.onSurface,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
const Settings(),
|
||||
builder:
|
||||
(BuildContext context) =>
|
||||
const Settings(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -267,9 +261,7 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: _buildViews(context),
|
||||
),
|
||||
body: SafeArea(child: _buildViews(context)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,6 +169,8 @@ class _FogotPasswordState extends State<FogotPassword> {
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -39,23 +39,25 @@ 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/')) {
|
||||
Provider.of<AppRepository>(context, listen: false)
|
||||
.signInWithCallback(uri)
|
||||
.then((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => const DeckLayout(),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
});
|
||||
}).catchError((_) {});
|
||||
if (mounted) {
|
||||
Provider.of<AppRepository>(context, listen: false)
|
||||
.signInWithCallback(uri)
|
||||
.then((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => const DeckLayout(),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
});
|
||||
}).catchError((_) {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -195,6 +203,8 @@ class ItemDetails extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
@@ -202,7 +212,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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'item_audio_player_init_stub.dart'
|
||||
if (dart.library.io) 'item_audio_player_init_native.dart'
|
||||
if (dart.library.html) 'item_audio_player_init_web.dart';
|
||||
|
||||
abstract class ItemAudioPlayerInit {
|
||||
void init();
|
||||
|
||||
factory ItemAudioPlayerInit() => getItemAudioPlayerInit();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:just_audio_media_kit/just_audio_media_kit.dart';
|
||||
|
||||
import 'item_audio_player_init.dart';
|
||||
|
||||
class ItemAudioPlayerInitNative implements ItemAudioPlayerInit {
|
||||
@override
|
||||
void init() {
|
||||
JustAudioMediaKit.ensureInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
ItemAudioPlayerInit getItemAudioPlayerInit() => ItemAudioPlayerInitNative();
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'item_audio_player_init.dart';
|
||||
|
||||
ItemAudioPlayerInit getItemAudioPlayerInit() => throw UnsupportedError(
|
||||
'Can not ItemAudioPlayerInit without the packages dart:html or dart:io',
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'item_audio_player_init.dart';
|
||||
|
||||
class ItemAudioPlayerInitWeb implements ItemAudioPlayerInit {
|
||||
@override
|
||||
void init() {}
|
||||
}
|
||||
|
||||
ItemAudioPlayerInit getItemAudioPlayerInit() => ItemAudioPlayerInitWeb();
|
||||
@@ -62,7 +62,7 @@ class _ItemAudioPlayerSeekBarState extends State<ItemAudioPlayerSeekBar> {
|
||||
data: _sliderThemeData.copyWith(
|
||||
thumbShape: HiddenThumbComponentShape(),
|
||||
activeTrackColor: Constants.secondary,
|
||||
inactiveTrackColor: Constants.backgroundContainerBackgroundColor,
|
||||
inactiveTrackColor: Constants.surfaceContainerBackgroundColor,
|
||||
),
|
||||
child: ExcludeSemantics(
|
||||
child: Slider(
|
||||
|
||||
@@ -10,11 +10,7 @@ import 'package:feeddeck/widgets/utils/cached_network_image.dart';
|
||||
|
||||
/// The [DescriptionFormat] enum defines the source and target format of a
|
||||
/// description.
|
||||
enum DescriptionFormat {
|
||||
html,
|
||||
markdown,
|
||||
plain,
|
||||
}
|
||||
enum DescriptionFormat { html, markdown, plain }
|
||||
|
||||
/// The [ItemDescription] widget displays the description of an item. The
|
||||
/// provided [itemDescription] is converted from the [sourceFormat] to the
|
||||
@@ -51,8 +47,10 @@ class ItemDescription extends StatelessWidget {
|
||||
fontFamily: getMonospaceFontFamily(),
|
||||
backgroundColor: Constants.secondary,
|
||||
),
|
||||
codeblockDecoration: const BoxDecoration(
|
||||
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) {
|
||||
@@ -60,15 +58,17 @@ class ItemDescription extends StatelessWidget {
|
||||
_openUrl(href);
|
||||
}
|
||||
},
|
||||
// TODO: The "flutter_markdown" package is deprecated and we have to
|
||||
// replace it with an alternative.
|
||||
// See: https://pub.dev/packages/flutter_markdown
|
||||
// ignore: deprecated_member_use
|
||||
imageBuilder: (uri, title, alt) {
|
||||
if (disableImages == true) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: Constants.spacingMiddle),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
@@ -79,9 +79,7 @@ class ItemDescription extends StatelessWidget {
|
||||
isDismissible: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.black,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: double.infinity,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: double.infinity),
|
||||
builder: (BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
@@ -99,9 +97,7 @@ class ItemDescription extends StatelessWidget {
|
||||
top: Constants.spacingExtraSmall,
|
||||
right: Constants.spacingExtraSmall,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
),
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
@@ -132,10 +128,7 @@ class ItemDescription extends StatelessWidget {
|
||||
return SelectableText(
|
||||
content.trim(),
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,7 +151,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,18 @@
|
||||
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,84 @@
|
||||
import 'dart:ui_web' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:web/web.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 HTMLIFrameElement _iframeElement = HTMLIFrameElement();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_iframeElement.src = _convertVideoUrl(widget.videoUrl);
|
||||
_iframeElement.style.border = 'none';
|
||||
_iframeElement.allowFullscreen = true;
|
||||
|
||||
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);
|
||||