Compare commits
75 Commits
v1.4.1
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
337d861684 | ||
|
|
906c2a1754 | ||
|
|
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 |
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 |
27
.github/dependabot.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
@@ -6,6 +7,9 @@ updates:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
@@ -17,6 +21,9 @@ updates:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
groups:
|
||||
pub:
|
||||
patterns:
|
||||
@@ -28,6 +35,9 @@ updates:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
groups:
|
||||
docker:
|
||||
patterns:
|
||||
@@ -39,6 +49,23 @@ updates:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
groups:
|
||||
npm:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/supabase/email-templates"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
groups:
|
||||
npm:
|
||||
patterns:
|
||||
|
||||
29
.github/release.yaml
vendored
@@ -1,29 +0,0 @@
|
||||
name-template: "$RESOLVED_VERSION"
|
||||
tag-template: "$RESOLVED_VERSION"
|
||||
version-template: "v$MAJOR.$MINOR.$PATCH"
|
||||
categories:
|
||||
- title: "Added"
|
||||
labels:
|
||||
- "changelog: added"
|
||||
- title: "Fixed"
|
||||
labels:
|
||||
- "changelog: fixed"
|
||||
- title: "Changed"
|
||||
labels:
|
||||
- "changelog: changed"
|
||||
version-resolver:
|
||||
minor:
|
||||
labels:
|
||||
- "changelog: added"
|
||||
- "changelog: changed"
|
||||
patch:
|
||||
labels:
|
||||
- "changelog: fixed"
|
||||
default: patch
|
||||
category-template: "### $TITLE"
|
||||
change-template: '- #$NUMBER: $TITLE @$AUTHOR'
|
||||
template: |
|
||||
$CHANGES
|
||||
replacers:
|
||||
- search: ':warning:'
|
||||
replace: ':warning: _Breaking change:_ :warning:'
|
||||
274
.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 }}
|
||||
@@ -56,20 +63,24 @@ jobs:
|
||||
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,20 +98,22 @@ jobs:
|
||||
|
||||
supabase db push
|
||||
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
# supabase functions deploy add-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy delete-user-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
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 }}
|
||||
@@ -110,26 +123,30 @@ jobs:
|
||||
|
||||
supabase db push
|
||||
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy add-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy delete-user-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
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 on pull requests or
|
||||
# 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.
|
||||
# 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.event_name == 'pull_request' || 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:
|
||||
@@ -138,7 +155,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -170,11 +187,12 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.22.2'
|
||||
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: |
|
||||
@@ -187,7 +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')
|
||||
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 }}
|
||||
@@ -195,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-14
|
||||
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:
|
||||
@@ -209,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.22.2'
|
||||
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: |
|
||||
@@ -227,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
|
||||
@@ -243,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 (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.
|
||||
# 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:
|
||||
@@ -262,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.22.2'
|
||||
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: |
|
||||
@@ -310,16 +342,21 @@ 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 "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.
|
||||
# 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.
|
||||
# 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
|
||||
@@ -333,25 +370,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.22.2'
|
||||
channel: 'master'
|
||||
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: |
|
||||
@@ -381,17 +420,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/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.
|
||||
# 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-2019
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
runs-on: windows-latest
|
||||
if:
|
||||
github.event_name == 'pull_request' || (github.event_name == 'release' &&
|
||||
github.event.action == 'published')
|
||||
permissions:
|
||||
contents: write
|
||||
defaults:
|
||||
@@ -400,16 +444,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.22.2'
|
||||
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: |
|
||||
@@ -439,7 +484,9 @@ 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/feeddeck-windows-x86_64.zip
|
||||
@@ -454,33 +501,39 @@ 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-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.
|
||||
# The "iOS" job builds the Flutter iOS app on every pull request. This is only
|
||||
# used to test that the build of the iOS app works. The artifact of the build
|
||||
# isn't uploaded / used.
|
||||
ios:
|
||||
name: iOS
|
||||
runs-on: macos-14
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
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.22.2'
|
||||
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: |
|
||||
@@ -489,21 +542,25 @@ 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
|
||||
@@ -511,11 +568,12 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.22.2'
|
||||
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: |
|
||||
|
||||
54
.github/workflows/continuous-integration.yaml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
@@ -5,6 +6,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
flutter:
|
||||
@@ -16,21 +19,26 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.22.2'
|
||||
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: |
|
||||
flutter pub get
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
dart analyze --fatal-infos
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
flutter test
|
||||
@@ -38,16 +46,44 @@ jobs:
|
||||
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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Supabase
|
||||
uses: supabase/setup-cli@v1
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
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 test --allow-env --import-map=supabase/functions/import_map.json supabase/functions
|
||||
deno task test
|
||||
|
||||
65
.github/workflows/landingpage.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: Landing Page
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
# The "Build Landing Page" job builds our landing page.
|
||||
build-landing-page:
|
||||
name: Build Landing Page
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "landing"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
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@v5
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm run build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: ./landing/out
|
||||
|
||||
# The "Deploy Landing Page" job deploys our landing page to GitHub Pages.
|
||||
deploy-landing-page:
|
||||
name: Deploy Landing Page
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-landing-page
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
80
.github/workflows/release.yaml
vendored
@@ -1,81 +1,21 @@
|
||||
---
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
# The "Changelog" job creates a new release draft on GitHub.
|
||||
changelog:
|
||||
name: Changelog
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Update Changelog
|
||||
uses: release-drafter/release-drafter@v6
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: github.ref_type == 'tag'
|
||||
with:
|
||||
config-name: release.yaml
|
||||
disable-autolabeler: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# The "Build Landing Page" job builds our landing page.
|
||||
build-landing-page:
|
||||
name: Build Landing Page
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "landing"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
cache-dependency-path: landing/package-lock.json
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm run build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./landing/out
|
||||
|
||||
# The "Deploy Landing Page" job deploys our landing page to GitHub Pages.
|
||||
deploy-landing-page:
|
||||
name: Deploy Landing Page
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-landing-page
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
generate_release_notes: true
|
||||
make_latest: true
|
||||
|
||||
@@ -57,10 +57,10 @@ check your installed version:
|
||||
```sh
|
||||
$ flutter --version
|
||||
|
||||
Flutter 3.22.2 • channel stable • https://github.com/flutter/flutter.git
|
||||
Framework • revision 761747bfc5 (3 days ago) • 2024-06-05 22:15:13 +0200
|
||||
Engine • revision edd8546116
|
||||
Tools • Dart 3.4.3 • DevTools 2.34.3
|
||||
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
|
||||
|
||||
@@ -233,26 +233,26 @@ docker-compose up --build
|
||||
To build the Docker image, the following commands can be run:
|
||||
|
||||
```sh
|
||||
docker build -f supabase/functions/_cmd/Dockerfile -t ghcr.io/feeddeck/feeddeck:dev supabase/functions
|
||||
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:dev supabase/functions
|
||||
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:dev tools get-feed '{"type": "reddit", "options": {"reddit": "/r/kubernetes"}}'
|
||||
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 --import-map=supabase/functions/import_map.json supabase/functions
|
||||
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 --import-map=supabase/functions/import_map.json supabase/functions --coverage=coverage_deno
|
||||
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
|
||||
@@ -283,17 +283,17 @@ supabase secrets set --env-file supabase/.env
|
||||
supabase secrets list
|
||||
|
||||
# Deploy all functions
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy add-source-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy delete-user-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
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:
|
||||
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 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
|
||||
|
||||
25
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
|
||||
|
||||
8
app/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage
|
||||
2
app/.gitignore
vendored
@@ -5,9 +5,11 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
|
||||
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,88 +0,0 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
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 flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "app.feeddeck.feeddeck"
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
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 34
|
||||
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 (keystorePropertiesFile.exists()) {
|
||||
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 '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")
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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-8.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}
|
||||
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||
|
||||
includeBuild("${settings.ext.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.1.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
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")
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '12.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,68 +1,30 @@
|
||||
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
|
||||
- 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.30.2):
|
||||
- Flutter
|
||||
- PurchasesHybridCommon (= 11.1.0)
|
||||
- PurchasesHybridCommon (11.1.0):
|
||||
- RevenueCat (= 4.43.2)
|
||||
- RevenueCat (4.43.2)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- 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
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (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/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -71,68 +33,32 @@ SPEC REPOS:
|
||||
- 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/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||
audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
purchases_flutter: 42d5544e7730ea89a88cc2f008b7c700fd147052
|
||||
PurchasesHybridCommon: 4022d5944cb30ec44ba5159e42aa161fe0e30175
|
||||
RevenueCat: 3d934653b7e8b09af88fd47e9e84cfaf5d0a89ba
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
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: 016564c560c4c9dbcb210e12c7aa6039072645f1
|
||||
PODFILE CHECKSUM: a35dde46ea09af570b675187a949f5fa3bc82280
|
||||
|
||||
COCOAPODS: 1.15.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,6 +164,9 @@
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1510;
|
||||
@@ -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
|
||||
}
|
||||
@@ -5,6 +5,24 @@
|
||||
<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,
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<false/>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -18,8 +18,8 @@ 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/reset_password/reset_password.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
|
||||
/// are initialized, so that we can preserve the splash screen until we are done
|
||||
@@ -91,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`
|
||||
@@ -119,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
|
||||
@@ -159,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,
|
||||
),
|
||||
@@ -172,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: {
|
||||
@@ -210,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';
|
||||
@@ -236,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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -263,6 +270,46 @@ 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
|
||||
@@ -302,25 +349,29 @@ class FDSourceOptions {
|
||||
|
||||
factory FDSourceOptions.fromJson(Map<String, dynamic> responseData) {
|
||||
return FDSourceOptions(
|
||||
fourchan: responseData.containsKey('fourchan') &&
|
||||
responseData['fourchan'] != null
|
||||
? responseData['fourchan']
|
||||
: null,
|
||||
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,
|
||||
lemmy: responseData.containsKey('lemmy') && responseData['lemmy'] != null
|
||||
? responseData['lemmy']
|
||||
: 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']
|
||||
@@ -329,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']
|
||||
@@ -341,13 +393,15 @@ 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']
|
||||
@@ -377,4 +431,55 @@ class FDSourceOptions {
|
||||
'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
|
||||
@@ -423,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();
|
||||
}
|
||||
|
||||
@@ -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}).inFilter('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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -157,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(
|
||||
@@ -170,8 +163,8 @@ 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,
|
||||
@@ -182,10 +175,8 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
/// 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),
|
||||
),
|
||||
@@ -209,7 +200,7 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
color: Constants.onSecondary,
|
||||
color: Constants.onSurface,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
@@ -248,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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -269,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,
|
||||
),
|
||||
|
||||
@@ -43,19 +43,21 @@ class _HomeState extends State<Home> {
|
||||
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((_) {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -203,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,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
library item_audio_player_init;
|
||||
|
||||
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';
|
||||
|
||||
@@ -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,17 +47,10 @@ class ItemDescription extends StatelessWidget {
|
||||
fontFamily: getMonospaceFontFamily(),
|
||||
backgroundColor: Constants.secondary,
|
||||
),
|
||||
codeblockDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
),
|
||||
codeblockDecoration: const BoxDecoration(color: Constants.secondary),
|
||||
blockquoteDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Constants.primary,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
border: Border(left: BorderSide(color: Constants.primary, width: 1)),
|
||||
),
|
||||
),
|
||||
onTapLink: (text, href, title) {
|
||||
@@ -69,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(
|
||||
@@ -88,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,
|
||||
@@ -108,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();
|
||||
},
|
||||
@@ -141,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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
library item_piped_video;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'item_piped_video_stub.dart'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:html'; // ignore: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
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';
|
||||
|
||||
@@ -42,7 +42,7 @@ class ItemPipedVideoWeb extends StatefulWidget implements ItemPipedVideo {
|
||||
}
|
||||
|
||||
class _ItemPipedVideoWebState extends State<ItemPipedVideoWeb> {
|
||||
final IFrameElement _iframeElement = IFrameElement();
|
||||
final HTMLIFrameElement _iframeElement = HTMLIFrameElement();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -52,7 +52,6 @@ class _ItemPipedVideoWebState extends State<ItemPipedVideoWeb> {
|
||||
_iframeElement.style.border = 'none';
|
||||
_iframeElement.allowFullscreen = true;
|
||||
|
||||
// ignore: undefined_prefixed_name
|
||||
ui.platformViewRegistry.registerViewFactory(
|
||||
widget.videoUrl,
|
||||
(int viewId) => _iframeElement,
|
||||
@@ -62,9 +61,7 @@ class _ItemPipedVideoWebState extends State<ItemPipedVideoWeb> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: Constants.spacingMiddle),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Center(
|
||||
@@ -84,7 +81,4 @@ class _ItemPipedVideoWebState extends State<ItemPipedVideoWeb> {
|
||||
}
|
||||
|
||||
ItemPipedVideo getItemPipedVideo(String? imageUrl, String videoUrl) =>
|
||||
ItemPipedVideoWeb(
|
||||
imageUrl: imageUrl,
|
||||
videoUrl: videoUrl,
|
||||
);
|
||||
ItemPipedVideoWeb(imageUrl: imageUrl, videoUrl: videoUrl);
|
||||
|
||||
@@ -12,10 +12,7 @@ import 'package:feeddeck/utils/constants.dart';
|
||||
/// played by the user. If the [videos] list is empty or null, the widget will
|
||||
/// not be displayed.
|
||||
class ItemVideos extends StatelessWidget {
|
||||
const ItemVideos({
|
||||
super.key,
|
||||
required this.videos,
|
||||
});
|
||||
const ItemVideos({super.key, required this.videos});
|
||||
|
||||
final List<String>? videos;
|
||||
|
||||
@@ -38,10 +35,7 @@ class ItemVideos extends StatelessWidget {
|
||||
/// The [ItemVideoQuality] class is used to store the different qualities of a
|
||||
/// video. It is used in combination with the [ItemVideoPlayer] widget.
|
||||
class ItemVideoQuality {
|
||||
const ItemVideoQuality({
|
||||
required this.quality,
|
||||
required this.video,
|
||||
});
|
||||
const ItemVideoQuality({required this.quality, required this.video});
|
||||
|
||||
final String quality;
|
||||
final String video;
|
||||
@@ -96,15 +90,13 @@ class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
|
||||
builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(
|
||||
Constants.spacingMiddle,
|
||||
),
|
||||
margin: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
padding: const EdgeInsets.only(
|
||||
left: Constants.spacingMiddle,
|
||||
right: Constants.spacingMiddle,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.background,
|
||||
color: Constants.surface,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
@@ -112,41 +104,42 @@ class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: widget.qualities!
|
||||
.asMap()
|
||||
.entries
|
||||
.map((quality) {
|
||||
if (quality.key == widget.qualities!.length - 1) {
|
||||
return [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
await _playerOpen(quality.value.video);
|
||||
},
|
||||
title: Text(quality.value.quality),
|
||||
),
|
||||
];
|
||||
}
|
||||
children:
|
||||
widget.qualities!
|
||||
.asMap()
|
||||
.entries
|
||||
.map((quality) {
|
||||
if (quality.key == widget.qualities!.length - 1) {
|
||||
return [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
await _playerOpen(quality.value.video);
|
||||
},
|
||||
title: Text(quality.value.quality),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
await _playerOpen(quality.value.video);
|
||||
},
|
||||
title: Text(quality.value.quality),
|
||||
),
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
];
|
||||
})
|
||||
.expand((e) => e)
|
||||
.toList(),
|
||||
return [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
await _playerOpen(quality.value.video);
|
||||
},
|
||||
title: Text(quality.value.quality),
|
||||
),
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
];
|
||||
})
|
||||
.expand((e) => e)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -209,20 +202,13 @@ class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
|
||||
/// [_playerOpen] opens the video player with the provided [video] and sets
|
||||
/// the audio track if it is provided via the [audio] parameter.
|
||||
Future<void> _playerOpen(String video) async {
|
||||
await player.open(
|
||||
Media(video),
|
||||
play: false,
|
||||
);
|
||||
await player.open(Media(video), play: false);
|
||||
|
||||
/// Load an external audio track when it is provided via the [audio]
|
||||
/// parameter.
|
||||
/// See: https://github.com/media-kit/media-kit?tab=readme-ov-file#load-external-audio-track
|
||||
if (widget.audio != null) {
|
||||
await player.setAudioTrack(
|
||||
AudioTrack.uri(
|
||||
widget.audio!,
|
||||
),
|
||||
);
|
||||
await player.setAudioTrack(AudioTrack.uri(widget.audio!));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +229,7 @@ class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: Constants.spacingMiddle),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Center(
|
||||
@@ -274,12 +258,13 @@ class _ItemVideoPlayerState extends State<ItemVideoPlayer> {
|
||||
),
|
||||
child: Video(
|
||||
controller: controller,
|
||||
controls: kIsWeb ||
|
||||
Platform.isLinux ||
|
||||
Platform.isMacOS ||
|
||||
Platform.isWindows
|
||||
? MaterialDesktopVideoControls
|
||||
: MaterialVideoControls,
|
||||
controls:
|
||||
kIsWeb ||
|
||||
Platform.isLinux ||
|
||||
Platform.isMacOS ||
|
||||
Platform.isWindows
|
||||
? MaterialDesktopVideoControls
|
||||
: MaterialVideoControls,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
library item_youtube_video;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'item_youtube_video_stub.dart'
|
||||
|
||||
@@ -6,6 +6,15 @@ import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
|
||||
import 'item_youtube_video.dart';
|
||||
|
||||
/// [FeFetchVideoUrlsResponse] is the model returned by the [_fetchVideoUrls]
|
||||
/// function. It contains a list of [videos] and an optional [audio] file.
|
||||
class FetchVideoUrlsResponse {
|
||||
const FetchVideoUrlsResponse({required this.videos, required this.audio});
|
||||
|
||||
final List<ItemVideoQuality> videos;
|
||||
final String? audio;
|
||||
}
|
||||
|
||||
class ItemYoutubeVideoNative extends StatefulWidget
|
||||
implements ItemYoutubeVideo {
|
||||
const ItemYoutubeVideoNative({
|
||||
@@ -23,21 +32,47 @@ class ItemYoutubeVideoNative extends StatefulWidget
|
||||
|
||||
class _ItemYoutubeVideoNativeState extends State<ItemYoutubeVideoNative> {
|
||||
final yt = YoutubeExplode();
|
||||
late Future<List<ItemVideoQuality>> _futureFetchVideoUrls;
|
||||
late Future<FetchVideoUrlsResponse> _futureFetchVideoUrls;
|
||||
|
||||
Future<List<ItemVideoQuality>> _fetchVideoUrls() async {
|
||||
/// [_fetchVideoUrls] fetches all video urls from YouTube via the
|
||||
/// [youtube_explode_dart] package. If the `muxed` field contains any items,
|
||||
/// we use them to return the video, because it contains the video and audio
|
||||
/// file. If the list is empty we use the `videoOnly` list to get the list of
|
||||
/// videos and the `audioOnly` field to get a corresponding audio track.
|
||||
Future<FetchVideoUrlsResponse> _fetchVideoUrls() async {
|
||||
final streamManifest = await yt.videos.streamsClient.getManifest(
|
||||
widget.videoUrl,
|
||||
);
|
||||
return streamManifest.muxed
|
||||
.sortByVideoQuality()
|
||||
.map(
|
||||
(element) => ItemVideoQuality(
|
||||
quality: element.qualityLabel,
|
||||
video: element.url.toString(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (streamManifest.muxed.isNotEmpty) {
|
||||
return FetchVideoUrlsResponse(
|
||||
videos:
|
||||
streamManifest.video
|
||||
.sortByVideoQuality()
|
||||
.map(
|
||||
(element) => ItemVideoQuality(
|
||||
quality: element.qualityLabel,
|
||||
video: element.url.toString(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
audio: null,
|
||||
);
|
||||
}
|
||||
|
||||
return FetchVideoUrlsResponse(
|
||||
videos:
|
||||
streamManifest.videoOnly
|
||||
.sortByVideoQuality()
|
||||
.map(
|
||||
(element) => ItemVideoQuality(
|
||||
quality: element.qualityLabel,
|
||||
video: element.url.toString(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
audio: streamManifest.audioOnly.sortByBitrate()[1].url.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -60,19 +95,20 @@ class _ItemYoutubeVideoNativeState extends State<ItemYoutubeVideoNative> {
|
||||
future: _futureFetchVideoUrls,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
AsyncSnapshot<List<ItemVideoQuality>> snapshot,
|
||||
AsyncSnapshot<FetchVideoUrlsResponse> snapshot,
|
||||
) {
|
||||
if (snapshot.connectionState == ConnectionState.none ||
|
||||
snapshot.connectionState == ConnectionState.waiting ||
|
||||
snapshot.hasError ||
|
||||
snapshot.data == null ||
|
||||
snapshot.data!.isEmpty) {
|
||||
snapshot.data!.videos.isEmpty) {
|
||||
return ItemMedia(itemMedia: widget.imageUrl);
|
||||
}
|
||||
|
||||
return ItemVideoPlayer(
|
||||
video: snapshot.data!.first.video,
|
||||
qualities: snapshot.data,
|
||||
video: snapshot.data!.videos.first.video,
|
||||
audio: snapshot.data!.audio,
|
||||
qualities: snapshot.data!.videos,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -80,7 +116,4 @@ class _ItemYoutubeVideoNativeState extends State<ItemYoutubeVideoNative> {
|
||||
}
|
||||
|
||||
ItemYoutubeVideo getItemYoutubeVideo(String? imageUrl, String videoUrl) =>
|
||||
ItemYoutubeVideoNative(
|
||||
imageUrl: imageUrl,
|
||||
videoUrl: videoUrl,
|
||||
);
|
||||
ItemYoutubeVideoNative(imageUrl: imageUrl, videoUrl: videoUrl);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:html'; // ignore: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:ui_web' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:web/web.dart';
|
||||
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'item_youtube_video.dart';
|
||||
|
||||
@@ -18,17 +18,11 @@ String _convertVideoUrl(String videoUrl) {
|
||||
}
|
||||
|
||||
if (videoUrl.startsWith('https://www.youtube.com/watch?v=')) {
|
||||
return 'https://www.youtube-nocookie.com/embed/${videoUrl.replaceFirst(
|
||||
'https://www.youtube.com/watch?v=',
|
||||
'',
|
||||
)}';
|
||||
return 'https://www.youtube-nocookie.com/embed/${videoUrl.replaceFirst('https://www.youtube.com/watch?v=', '')}';
|
||||
}
|
||||
|
||||
if (videoUrl.startsWith('https://m.youtube.com/watch?v=')) {
|
||||
return 'https://www.youtube-nocookie.com/embed/${videoUrl.replaceFirst(
|
||||
'https://m.youtube.com/watch?v=',
|
||||
'',
|
||||
)}';
|
||||
return 'https://www.youtube-nocookie.com/embed/${videoUrl.replaceFirst('https://m.youtube.com/watch?v=', '')}';
|
||||
}
|
||||
|
||||
return videoUrl;
|
||||
@@ -49,7 +43,7 @@ class ItemYoutubeVideoWeb extends StatefulWidget implements ItemYoutubeVideo {
|
||||
}
|
||||
|
||||
class _ItemYoutubeVideoWebState extends State<ItemYoutubeVideoWeb> {
|
||||
final IFrameElement _iframeElement = IFrameElement();
|
||||
final HTMLIFrameElement _iframeElement = HTMLIFrameElement();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -59,7 +53,6 @@ class _ItemYoutubeVideoWebState extends State<ItemYoutubeVideoWeb> {
|
||||
_iframeElement.style.border = 'none';
|
||||
_iframeElement.allowFullscreen = true;
|
||||
|
||||
// ignore: undefined_prefixed_name
|
||||
ui.platformViewRegistry.registerViewFactory(
|
||||
widget.videoUrl,
|
||||
(int viewId) => _iframeElement,
|
||||
@@ -69,9 +62,7 @@ class _ItemYoutubeVideoWebState extends State<ItemYoutubeVideoWeb> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: Constants.spacingMiddle),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Center(
|
||||
@@ -91,7 +82,4 @@ class _ItemYoutubeVideoWebState extends State<ItemYoutubeVideoWeb> {
|
||||
}
|
||||
|
||||
ItemYoutubeVideo getItemYoutubeVideo(String? imageUrl, String videoUrl) =>
|
||||
ItemYoutubeVideoWeb(
|
||||
imageUrl: imageUrl,
|
||||
videoUrl: videoUrl,
|
||||
);
|
||||
ItemYoutubeVideoWeb(imageUrl: imageUrl, videoUrl: videoUrl);
|
||||
|
||||
@@ -195,7 +195,7 @@ class _ItemActionsState extends State<ItemActions> {
|
||||
right: Constants.spacingMiddle,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.background,
|
||||
color: Constants.surface,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
@@ -41,9 +37,7 @@ class ItemDescription extends StatelessWidget {
|
||||
/// [_buildMarkdown] renders the provided [content] as markdown.
|
||||
Widget _buildMarkdown(String content) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingExtraSmall,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: Constants.spacingExtraSmall),
|
||||
child: MarkdownBody(
|
||||
selectable: false,
|
||||
data: content.trim(),
|
||||
@@ -52,16 +46,11 @@ class ItemDescription extends StatelessWidget {
|
||||
fontFamily: getMonospaceFontFamily(),
|
||||
backgroundColor: Constants.secondary,
|
||||
),
|
||||
codeblockDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
),
|
||||
codeblockDecoration: const BoxDecoration(color: Constants.secondary),
|
||||
blockquoteDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Constants.primary,
|
||||
width: 1,
|
||||
),
|
||||
left: BorderSide(color: Constants.primary, width: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -70,11 +59,13 @@ 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) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingExtraSmall,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: Constants.spacingExtraSmall),
|
||||
child: CachedNetworkImage(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
@@ -100,9 +91,7 @@ class ItemDescription extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingExtraSmall,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: Constants.spacingExtraSmall),
|
||||
child: Text(
|
||||
content.trim().split('\n').where((line) => line != '').join('\n'),
|
||||
maxLines: 5,
|
||||
|
||||
@@ -125,6 +125,8 @@ class _ResetPasswordState extends State<ResetPassword> {
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
@@ -152,6 +154,8 @@ class _ResetPasswordState extends State<ResetPassword> {
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -235,6 +235,8 @@ class _SetSettingsState extends State<SetSettings> {
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
@@ -253,6 +255,8 @@ class _SetSettingsState extends State<SetSettings> {
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -290,6 +290,8 @@ class _SettingsAccountsGithubAddState extends State<SettingsAccountsGithubAdd> {
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -28,7 +28,7 @@ class SettingsAccountsActions extends StatelessWidget {
|
||||
right: Constants.spacingMiddle,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.background,
|
||||
color: Constants.surface,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
|
||||
29
app/lib/widgets/settings/app_settings/app_settings.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/settings/app_settings/app_settings_export.dart';
|
||||
import 'package:feeddeck/widgets/settings/app_settings/app_settings_import.dart';
|
||||
|
||||
/// The [SettSettingsAppSettings] widget is used to display a list of all
|
||||
/// available app settings, which can be used to customize the app by the user
|
||||
/// and to import and export data.
|
||||
class SettingsAppSettings extends StatelessWidget {
|
||||
const SettingsAppSettings({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'App Settings',
|
||||
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: Constants.spacingSmall),
|
||||
SettingsAppSettingsExport(),
|
||||
SettingsAppSettingsImport(),
|
||||
SizedBox(height: Constants.spacingMiddle),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
356
app/lib/widgets/settings/app_settings/app_settings_export.dart
Normal file
@@ -0,0 +1,356 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import 'package:feeddeck/models/column.dart';
|
||||
import 'package:feeddeck/models/deck.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/repositories/app_repository.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/general/elevated_button_progress_indicator.dart';
|
||||
|
||||
class SettingsAppSettingsExport extends StatelessWidget {
|
||||
const SettingsAppSettingsExport({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: Constants.centeredFormMaxWidth,
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return Export();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
color: Constants.secondary,
|
||||
margin: const EdgeInsets.only(bottom: Constants.spacingSmall),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Characters('Export')
|
||||
.replaceAll(
|
||||
Characters(''),
|
||||
Characters('\u{200B}'),
|
||||
)
|
||||
.toString(),
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.download),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Export extends StatefulWidget {
|
||||
const Export({super.key});
|
||||
|
||||
@override
|
||||
State<Export> createState() => _ExportState();
|
||||
}
|
||||
|
||||
class _ExportState extends State<Export> {
|
||||
FDDeck? _selectedDeck;
|
||||
bool _isLoading = false;
|
||||
String _success = '';
|
||||
String _error = '';
|
||||
|
||||
/// [_export] is the function to export a deck as OPML file. In the first step
|
||||
/// we have to get the columns for the selected deck and all sources for all
|
||||
/// the columns of the deck. Afterwards we create the OPML file and save it.
|
||||
Future<void> _export() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_success = '';
|
||||
_error = '';
|
||||
});
|
||||
|
||||
/// If the [_selectedDeckId] is an empty string the user didn't selected a
|
||||
/// deck which should be exported, so that we return an error.
|
||||
if (_selectedDeck == null) {
|
||||
setState(() {
|
||||
_error = 'Please select a deck to export.';
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
/// Get all the columns for the [_selectedDeckId].
|
||||
final data = await Supabase.instance.client
|
||||
.from('columns')
|
||||
.select('id, name, position')
|
||||
.eq('deckId', _selectedDeck!.id)
|
||||
.order('position', ascending: true);
|
||||
final columns = List<FDColumn>.from(
|
||||
data.map((column) => FDColumn.fromJson(column)),
|
||||
);
|
||||
|
||||
/// Loop through all the columns and get the sources for each column. The
|
||||
/// sources are then added to the `sources` field of the column.
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
final data = await Supabase.instance.client
|
||||
.from('sources')
|
||||
.select('id, type, title, options, link, icon')
|
||||
.eq('columnId', columns[i].id)
|
||||
.order('position', ascending: true, nullsFirst: false)
|
||||
.order('createdAt', ascending: true);
|
||||
columns[i].sources = List<FDSource>.from(
|
||||
data.map((source) => FDSource.fromJson(source)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the OPML file. Here we only add the `opml` tag with the `head`
|
||||
/// and `body` tags. The `head` tag contains the deck name within the
|
||||
/// `title` tag. To fill the body we go through all columns to create a
|
||||
/// `outline` tag for each column and a nested `outline` tag for each
|
||||
/// souce. This is handled by the [toXml] methhod of the corresponding
|
||||
/// models.
|
||||
var builder = XmlBuilder();
|
||||
builder.processing('xml', 'version="1.0" encoding="utf-8"');
|
||||
builder.element(
|
||||
'opml',
|
||||
nest: () {
|
||||
builder.attribute('version', '2.0');
|
||||
builder.element(
|
||||
'head',
|
||||
nest: () {
|
||||
builder.element(
|
||||
'title',
|
||||
nest: () {
|
||||
builder.text(_selectedDeck!.name);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
builder.element(
|
||||
'body',
|
||||
nest: () {
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
columns[i].toXml(builder);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
final opml = builder.buildDocument().toXmlString(pretty: true);
|
||||
|
||||
/// On native platforms the file name should not contain the extension,
|
||||
/// otherwise the extension is added twice. On the web the file name must
|
||||
/// contain the extension.
|
||||
String fileName = 'feeddeck-export-${_selectedDeck!.id}';
|
||||
if (kIsWeb) {
|
||||
fileName = 'feeddeck-export-${_selectedDeck!.id}.opml';
|
||||
}
|
||||
|
||||
/// Open a file picker to let the user save the OPML file. When the file
|
||||
/// is saved successfully the file picker will return the file path on
|
||||
/// native platforms. If the file path is `null` the user aborted the
|
||||
/// export. On the web the file path will always be empty, so that we can
|
||||
/// directly display a [_success] message.
|
||||
final filePath = await FilePicker.platform.saveFile(
|
||||
allowedExtensions: ['opml'],
|
||||
type: FileType.custom,
|
||||
dialogTitle: 'Export',
|
||||
fileName: fileName,
|
||||
lockParentWindow: true,
|
||||
bytes: utf8.encode(opml),
|
||||
);
|
||||
|
||||
if (kIsWeb) {
|
||||
setState(() {
|
||||
_success = 'Export successful';
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (filePath == null) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_success = 'Export successful: $filePath';
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (err) {
|
||||
setState(() {
|
||||
_error = 'Export failed: ${err.toString()}';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// [_buildSuccess] returns a widget to display the [_success] when it is not
|
||||
/// an empty string.
|
||||
Widget _buildSuccess() {
|
||||
if (_success != '') {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Text(_success, style: const TextStyle(color: Constants.primary)),
|
||||
);
|
||||
}
|
||||
|
||||
return Container();
|
||||
}
|
||||
|
||||
/// [_buildError] returns a widget to display the [_error] when it is not an
|
||||
/// empty string.
|
||||
Widget _buildError() {
|
||||
if (_error != '') {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Text(_error, style: const TextStyle(color: Constants.error)),
|
||||
);
|
||||
}
|
||||
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppRepository app = Provider.of<AppRepository>(context, listen: true);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
shape: const Border(
|
||||
bottom: BorderSide(color: Constants.dividerColor, width: 1),
|
||||
),
|
||||
title: Text('Export'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Select a deck, which should be exported from the following list. Click the "Export" button afterwards to save the deck as OPML file.',
|
||||
),
|
||||
const SizedBox(height: Constants.spacingMiddle),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: app.decks.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RadioListTile(
|
||||
title: Text(app.decks[index].name),
|
||||
dense: true,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: VisualDensity.minimumDensity,
|
||||
vertical: VisualDensity.minimumDensity,
|
||||
),
|
||||
value: app.decks[index].id,
|
||||
groupValue: _selectedDeck?.id,
|
||||
onChanged: (String? deckId) {
|
||||
if (deckId != null) {
|
||||
setState(() {
|
||||
_selectedDeck = app.decks[index];
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Constants.spacingSmall),
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
_buildSuccess(),
|
||||
_buildError(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
minimumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
),
|
||||
label: const Text('Export'),
|
||||
onPressed: _isLoading ? null : _export,
|
||||
icon:
|
||||
_isLoading
|
||||
? const ElevatedButtonProgressIndicator()
|
||||
: const Icon(Icons.download),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
435
app/lib/widgets/settings/app_settings/app_settings_import.dart
Normal file
@@ -0,0 +1,435 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import 'package:feeddeck/models/column.dart';
|
||||
import 'package:feeddeck/models/deck.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/repositories/app_repository.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/general/elevated_button_progress_indicator.dart';
|
||||
|
||||
class SettingsAppSettingsImport extends StatelessWidget {
|
||||
const SettingsAppSettingsImport({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: Constants.centeredFormMaxWidth,
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return Import();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
color: Constants.secondary,
|
||||
margin: const EdgeInsets.only(bottom: Constants.spacingSmall),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Characters('Import')
|
||||
.replaceAll(
|
||||
Characters(''),
|
||||
Characters('\u{200B}'),
|
||||
)
|
||||
.toString(),
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.upload),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Import extends StatefulWidget {
|
||||
const Import({super.key});
|
||||
|
||||
@override
|
||||
State<Import> createState() => _ExportState();
|
||||
}
|
||||
|
||||
class _ExportState extends State<Import> {
|
||||
bool _isLoading = false;
|
||||
String _success = '';
|
||||
String _error = '';
|
||||
int _progressMax = 0;
|
||||
int _progressCurrent = 0;
|
||||
|
||||
/// [_import] is the function to import a deck from a OPML file. In the first
|
||||
/// step we let the user select a file. Then we parse the file and create a
|
||||
/// new deck with all it's columns and sources.
|
||||
Future<void> _import() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_success = '';
|
||||
_error = '';
|
||||
_progressMax = 0;
|
||||
_progressCurrent = 0;
|
||||
});
|
||||
|
||||
try {
|
||||
/// Show a file picker, where the user can select an OPML file, which will
|
||||
/// be parsed in the following.
|
||||
///
|
||||
/// - For Android we have to unset the [allowedExtensions] and [type],
|
||||
/// because of https://github.com/miguelpruivo/flutter_file_picker/issues/1689
|
||||
/// - For iOS we have to unset the [allowedExtensions] and [type],
|
||||
/// otherwise we can not select files in the file browser.
|
||||
final files =
|
||||
(await FilePicker.platform.pickFiles(
|
||||
allowedExtensions:
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS)
|
||||
? null
|
||||
: ['opml'],
|
||||
type:
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS)
|
||||
? FileType.any
|
||||
: FileType.custom,
|
||||
allowMultiple: false,
|
||||
dialogTitle: 'Import',
|
||||
lockParentWindow: true,
|
||||
withData: true,
|
||||
))?.files;
|
||||
|
||||
/// If the list of selected files is empty or doesn't has a length of 1 or
|
||||
/// the selected file is empty we return here. This can happen if the user
|
||||
/// aborted the import and closed the file picker without selecting a
|
||||
/// file.
|
||||
if (files == null || files.length != 1 || files[0].bytes == null) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/// Parse the selected OPML file and get the name of the deck which should
|
||||
/// be created and all the columns and sources.
|
||||
final opml = XmlDocument.parse(utf8.decode(files[0].bytes!));
|
||||
|
||||
final deckName =
|
||||
opml
|
||||
.getElement('opml')
|
||||
?.getElement('head')
|
||||
?.getElement('title')
|
||||
?.innerText ??
|
||||
'Unknown';
|
||||
|
||||
final columns = <FDColumn>[];
|
||||
final unknownColumn = FDColumn(
|
||||
id: '',
|
||||
name: 'Unknown',
|
||||
position: 0,
|
||||
sources: [],
|
||||
);
|
||||
|
||||
opml
|
||||
.getElement('opml')
|
||||
?.getElement('body')
|
||||
?.findElements('outline')
|
||||
.forEach((outline) {
|
||||
/// If the `outline` element contains a `type` attribute, it must be
|
||||
/// a source. This can happen if an OPML file from another RSS
|
||||
/// reader is imported, which doesn't categorize the RSS feed.
|
||||
///
|
||||
/// In this case we try to parse the `outline` element as source and
|
||||
/// add the source to the [unknownColumn] column.
|
||||
if (outline.getAttribute('type') != null) {
|
||||
final source = FDSource.fromXml(outline);
|
||||
if (source.type != FDSourceType.none) {
|
||||
unknownColumn.sources.add(source);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/// If the `outline` element doesn't contain a `type` attribute, it
|
||||
/// must be a column. This is the case for all exports from FeedDeck
|
||||
/// and for OPML files from other vendors, which categorize their
|
||||
/// feeds.
|
||||
///
|
||||
/// In this case we parse the `outline` element as column and add it
|
||||
/// to the [columns]. Within the column parsing we also try to parse
|
||||
/// all sibling `outline` elements as source and add it to the
|
||||
/// column.
|
||||
final column = FDColumn.fromXml(outline);
|
||||
columns.add(column);
|
||||
return;
|
||||
});
|
||||
|
||||
/// If we have added sources to the [unknownColumn] column, we add it to
|
||||
/// the list of [columns]. Otherwise we can ignore it.
|
||||
if (unknownColumn.sources.isNotEmpty) {
|
||||
columns.add(unknownColumn);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_progressMax = columns
|
||||
.map((column) => column.sources.length)
|
||||
.reduce((a, b) => a + b);
|
||||
});
|
||||
|
||||
/// Create a new deck with the name ([deckName]) we retrieved earlier from
|
||||
/// the selected OPML file.
|
||||
final newDeck = FDDeck.fromJson(
|
||||
await Supabase.instance.client
|
||||
.from('decks')
|
||||
.insert({
|
||||
'name': deckName,
|
||||
'userId': Supabase.instance.client.auth.currentUser!.id,
|
||||
})
|
||||
.select()
|
||||
.single(),
|
||||
);
|
||||
|
||||
final errors = <String>[];
|
||||
|
||||
/// Go through all the columns and sources we got from the OPML file and
|
||||
/// create them in the newly created deck. If this process throws an error
|
||||
/// we do not abort the import, instead we add the error to the list of
|
||||
/// [errors].
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
try {
|
||||
final newColumn = FDColumn.fromJson(
|
||||
await Supabase.instance.client
|
||||
.from('columns')
|
||||
.insert({
|
||||
'deckId': newDeck.id,
|
||||
'userId': Supabase.instance.client.auth.currentUser!.id,
|
||||
'name': columns[i].name,
|
||||
'position': i,
|
||||
})
|
||||
.select()
|
||||
.single(),
|
||||
);
|
||||
|
||||
for (var j = 0; j < columns[i].sources.length; j++) {
|
||||
try {
|
||||
final result = await Supabase.instance.client.functions.invoke(
|
||||
'add-or-update-source-v1',
|
||||
body: {
|
||||
'source': {
|
||||
'id': '',
|
||||
'columnId': newColumn.id,
|
||||
'userId': '',
|
||||
'type': columns[i].sources[j].type.toShortString(),
|
||||
'title': '',
|
||||
'options': columns[i].sources[j].options?.toJson(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status != 200) {
|
||||
errors.add('Failed to create source: ${result.data['error']}');
|
||||
}
|
||||
} catch (err) {
|
||||
errors.add('Failed to create source: ${err.toString()}');
|
||||
} finally {
|
||||
setState(() {
|
||||
_progressCurrent++;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
errors.add('Failed to create column: ${err.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// When we have created the new deck with all it's columns and sources,
|
||||
/// we trigger an update of the decks in the [AppRepository] so that the
|
||||
/// new deck is also visible in the UI.
|
||||
if (mounted) {
|
||||
await Provider.of<AppRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).getDecksWithNotifiy();
|
||||
}
|
||||
|
||||
/// While creating all columns and sources we add the errors to the
|
||||
/// [errors] list. If the list is empty everthing went fine. If the list
|
||||
/// contains an error we show the error to the user.
|
||||
if (errors.isNotEmpty) {
|
||||
setState(() {
|
||||
_success = 'Import successfull, with ${errors.length} errors';
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_success = 'Import successfull';
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (err) {
|
||||
setState(() {
|
||||
_error = 'Import failed: ${err.toString()}';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// [_buildSuccess] returns a widget to display the [_success] when it is not
|
||||
/// an empty string.
|
||||
Widget _buildSuccess() {
|
||||
if (_success != '') {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Text(_success, style: const TextStyle(color: Constants.primary)),
|
||||
);
|
||||
}
|
||||
|
||||
return Container();
|
||||
}
|
||||
|
||||
/// [_buildError] returns a widget to display the [_error] when it is not an
|
||||
/// empty string.
|
||||
Widget _buildError() {
|
||||
if (_error != '') {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Text(_error, style: const TextStyle(color: Constants.error)),
|
||||
);
|
||||
}
|
||||
|
||||
return Container();
|
||||
}
|
||||
|
||||
/// [_buildProgress] returns a widget to display the progress when the
|
||||
/// [_progressMax] value if not 0.
|
||||
Widget _buildProgress() {
|
||||
if (_progressMax > 0) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Importing source $_progressCurrent of $_progressMax',
|
||||
style: const TextStyle(color: Constants.primary),
|
||||
),
|
||||
LinearProgressIndicator(value: _progressCurrent / _progressMax),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
shape: const Border(
|
||||
bottom: BorderSide(color: Constants.dividerColor, width: 1),
|
||||
),
|
||||
title: Text('Import'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Click the "Import" button to select an OPML file. This action will create a new deck containing all the columns and sources from the OPML file.',
|
||||
),
|
||||
const SizedBox(height: Constants.spacingMiddle),
|
||||
_buildProgress(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Constants.spacingSmall),
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
_buildSuccess(),
|
||||
_buildError(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
minimumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
),
|
||||
label: const Text('Import'),
|
||||
onPressed: _isLoading ? null : _import,
|
||||
icon:
|
||||
_isLoading
|
||||
? const ElevatedButtonProgressIndicator()
|
||||
: const Icon(Icons.upload),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -77,18 +77,19 @@ class _SettingsDecksEditExistingState extends State<SettingsDecksEditExisting> {
|
||||
/// [_showDeleteDialog] creates a new dialog, which is shown before the deck
|
||||
/// can be deleted. This is done to raise the awareness that the deck,
|
||||
/// columns, sources and items which belongs to the deck 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 Deck'),
|
||||
@@ -161,12 +162,7 @@ class _SettingsDecksEditExistingState extends State<SettingsDecksEditExisting> {
|
||||
left: Constants.spacingMiddle,
|
||||
right: Constants.spacingMiddle,
|
||||
),
|
||||
child: Text(
|
||||
_error,
|
||||
style: const TextStyle(
|
||||
color: Constants.error,
|
||||
),
|
||||
),
|
||||
child: Text(_error, style: const TextStyle(color: Constants.error)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,19 +173,13 @@ class _SettingsDecksEditExistingState extends State<SettingsDecksEditExisting> {
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Constants.secondary,
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: Constants.spacingSmall,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.only(bottom: Constants.spacingSmall),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
Constants.spacingMiddle,
|
||||
),
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Row(
|
||||
|
||||
@@ -33,15 +33,13 @@ class _SettingsPremiumInAppState extends State<SettingsPremiumInApp> {
|
||||
Future<Offering?> _fetchOfferings() async {
|
||||
if (Platform.isAndroid) {
|
||||
await Purchases.configure(
|
||||
PurchasesConfiguration(
|
||||
SettingsRepository().revenueCatGooglePlayKey,
|
||||
)..appUserID = supabase.Supabase.instance.client.auth.currentUser!.id,
|
||||
PurchasesConfiguration(SettingsRepository().revenueCatGooglePlayKey)
|
||||
..appUserID = supabase.Supabase.instance.client.auth.currentUser!.id,
|
||||
);
|
||||
} else if (Platform.isMacOS || Platform.isIOS) {
|
||||
await Purchases.configure(
|
||||
PurchasesConfiguration(
|
||||
SettingsRepository().revenueCatAppStoreKey,
|
||||
)..appUserID = supabase.Supabase.instance.client.auth.currentUser!.id,
|
||||
PurchasesConfiguration(SettingsRepository().revenueCatAppStoreKey)
|
||||
..appUserID = supabase.Supabase.instance.client.auth.currentUser!.id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,16 +60,22 @@ class _SettingsPremiumInAppState extends State<SettingsPremiumInApp> {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
CustomerInfo customerInfo = await Purchases.purchasePackage(package);
|
||||
final purchaseResult = await Purchases.purchasePackage(package);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
if (!customerInfo.entitlements.all.containsKey('FeedDeck Premium')) {
|
||||
if (!purchaseResult.customerInfo.entitlements.all.containsKey(
|
||||
'FeedDeck Premium',
|
||||
)) {
|
||||
throw Exception('FeedDeck Premium entitlement not found.');
|
||||
}
|
||||
|
||||
if (customerInfo.entitlements.all['FeedDeck Premium']!.isActive) {
|
||||
if (purchaseResult
|
||||
.customerInfo
|
||||
.entitlements
|
||||
.all['FeedDeck Premium']!
|
||||
.isActive) {
|
||||
if (!mounted) return;
|
||||
Provider.of<ProfileRepository>(
|
||||
context,
|
||||
@@ -149,17 +153,12 @@ class _SettingsPremiumInAppState extends State<SettingsPremiumInApp> {
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
shape: const Border(
|
||||
bottom: BorderSide(
|
||||
color: Constants.dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
bottom: BorderSide(color: Constants.dividerColor, width: 1),
|
||||
),
|
||||
title: const Text('FeedDeck Premium'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
),
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
@@ -169,17 +168,15 @@ class _SettingsPremiumInAppState extends State<SettingsPremiumInApp> {
|
||||
body: SafeArea(
|
||||
child: FutureBuilder(
|
||||
future: _futureFetchOfferings,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
AsyncSnapshot<Offering?> snapshot,
|
||||
) {
|
||||
builder: (BuildContext context, AsyncSnapshot<Offering?> snapshot) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: SingleChildScrollView(
|
||||
child: snapshot.connectionState == ConnectionState.none ||
|
||||
child:
|
||||
snapshot.connectionState == ConnectionState.none ||
|
||||
snapshot.connectionState ==
|
||||
ConnectionState.waiting ||
|
||||
snapshot.hasError ||
|
||||
@@ -188,7 +185,8 @@ class _SettingsPremiumInAppState extends State<SettingsPremiumInApp> {
|
||||
? const Text('Loading ...')
|
||||
: MarkdownBody(
|
||||
selectable: true,
|
||||
data: '''
|
||||
data:
|
||||
'''
|
||||
You are currently using the free version of FeedDeck, which allows you to add up
|
||||
to 10 sources for the first 7 days. After that trial period your sources will
|
||||
not be updated anymore.
|
||||
@@ -202,9 +200,7 @@ canceled at any time.
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingSmall,
|
||||
),
|
||||
const SizedBox(height: Constants.spacingSmall),
|
||||
const Divider(
|
||||
color: Constants.dividerColor,
|
||||
height: 1,
|
||||
@@ -230,15 +226,16 @@ canceled at any time.
|
||||
),
|
||||
onPressed:
|
||||
snapshot.connectionState == ConnectionState.none ||
|
||||
snapshot.connectionState ==
|
||||
ConnectionState.waiting ||
|
||||
snapshot.hasError ||
|
||||
snapshot.data == null ||
|
||||
snapshot.data?.monthly == null ||
|
||||
_isLoading
|
||||
? null
|
||||
: () => _purchase(snapshot.data!.monthly!),
|
||||
icon: snapshot.connectionState == ConnectionState.none ||
|
||||
snapshot.connectionState ==
|
||||
ConnectionState.waiting ||
|
||||
snapshot.hasError ||
|
||||
snapshot.data == null ||
|
||||
snapshot.data?.monthly == null ||
|
||||
_isLoading
|
||||
? null
|
||||
: () => _purchase(snapshot.data!.monthly!),
|
||||
icon:
|
||||
snapshot.connectionState == ConnectionState.none ||
|
||||
snapshot.connectionState ==
|
||||
ConnectionState.waiting ||
|
||||
snapshot.hasError ||
|
||||
|
||||
@@ -212,6 +212,8 @@ class _SettingsProfileCustomerPortalModalState
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -24,18 +24,19 @@ class _SettingsProfileDeleteAccountState
|
||||
/// [_showDeleteDialog] creates a new dialog, which is shown before the user
|
||||
/// account is deleted. This is done to raise the awareness what it means to
|
||||
/// delete the account and to avoid accidently deletions.
|
||||
_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 Account'),
|
||||
@@ -94,9 +95,7 @@ class _SettingsProfileDeleteAccountState
|
||||
if (!mounted) return;
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => const SignIn(),
|
||||
),
|
||||
MaterialPageRoute(builder: (BuildContext context) => const SignIn()),
|
||||
(route) => false,
|
||||
);
|
||||
} on ApiException catch (err) {
|
||||
@@ -136,9 +135,7 @@ class _SettingsProfileDeleteAccountState
|
||||
onTap: () => _showDeleteDialog(),
|
||||
child: Card(
|
||||
color: Constants.secondary,
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: Constants.spacingSmall,
|
||||
),
|
||||
margin: const EdgeInsets.only(bottom: Constants.spacingSmall),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -146,9 +143,7 @@ class _SettingsProfileDeleteAccountState
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
Constants.spacingMiddle,
|
||||
),
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -172,12 +167,7 @@ class _SettingsProfileDeleteAccountState
|
||||
],
|
||||
),
|
||||
),
|
||||
buildIcon(
|
||||
const Icon(
|
||||
Icons.delete,
|
||||
color: Constants.error,
|
||||
),
|
||||
),
|
||||
buildIcon(const Icon(Icons.delete, color: Constants.error)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -162,7 +162,7 @@ class SettingsProfileSignOutActions extends StatelessWidget {
|
||||
right: Constants.spacingMiddle,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Constants.background,
|
||||
color: Constants.surface,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:feeddeck/repositories/profile_repository.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/settings/accounts/settings_accounts.dart';
|
||||
import 'package:feeddeck/widgets/settings/app_settings/app_settings.dart';
|
||||
import 'package:feeddeck/widgets/settings/decks/settings_decks.dart';
|
||||
import 'package:feeddeck/widgets/settings/premium/settings_premium.dart';
|
||||
import 'package:feeddeck/widgets/settings/profile/settings_profile.dart';
|
||||
@@ -30,30 +31,27 @@ class _SettingsState extends State<Settings> {
|
||||
/// call the function multiple times we set the `force` parameter to
|
||||
/// `false`.
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
Provider.of<ProfileRepository>(context, listen: false)
|
||||
.init(false)
|
||||
.then((_) => {});
|
||||
Provider.of<ProfileRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).init(false).then((_) => {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Constants.background,
|
||||
color: Constants.surface,
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
),
|
||||
appBar: AppBar(title: const Text('Settings')),
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: Constants.centeredFormMaxWidth,
|
||||
),
|
||||
padding: const EdgeInsets.all(
|
||||
Constants.spacingMiddle,
|
||||
),
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -75,6 +73,10 @@ class _SettingsState extends State<Settings> {
|
||||
/// his account.
|
||||
SettingsProfile(),
|
||||
|
||||
/// Display the app settings. Here the user can customize
|
||||
/// the app and import / export data.
|
||||
SettingsAppSettings(),
|
||||
|
||||
/// Display some general information about the app, like the
|
||||
/// version and the link to our website.
|
||||
SettingsInfo(),
|
||||
|
||||
@@ -339,7 +339,10 @@ class _SignInState extends State<SignIn> {
|
||||
onPressed: _isLoading ? null : () => _signInWithGoogle(),
|
||||
icon: _isLoading
|
||||
? const ElevatedButtonProgressIndicator()
|
||||
: const Icon(FDIcons.google),
|
||||
: const Icon(
|
||||
FDIcons.google,
|
||||
color: Color(0xffffffff),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingMiddle,
|
||||
@@ -362,7 +365,10 @@ class _SignInState extends State<SignIn> {
|
||||
onPressed: _isLoading ? null : () => _signInWithApple(),
|
||||
icon: _isLoading
|
||||
? const ElevatedButtonProgressIndicator()
|
||||
: const Icon(FDIcons.apple),
|
||||
: const Icon(
|
||||
FDIcons.apple,
|
||||
color: Color(0xff000000),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingMiddle,
|
||||
@@ -395,7 +401,10 @@ class _SignInState extends State<SignIn> {
|
||||
},
|
||||
icon: _isLoading
|
||||
? const ElevatedButtonProgressIndicator()
|
||||
: const Icon(FDIcons.feeddeck),
|
||||
: const Icon(
|
||||
FDIcons.feeddeck,
|
||||
color: Constants.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingMiddle,
|
||||
|
||||
@@ -194,6 +194,8 @@ class _SignInWithFeedDeckState extends State<SignInWithFeedDeck> {
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -231,6 +231,8 @@ class _SignUpState extends State<SignUp> {
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Constants.secondary,
|
||||
foregroundColor: Constants.onSecondary,
|
||||
maximumSize: const Size.fromHeight(
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
|
||||
@@ -151,10 +151,7 @@ class _AddSourceState extends State<AddSource> {
|
||||
style: TextStyle(
|
||||
/// Since we are using the brand color as background
|
||||
/// color, we are using the same color as for the icon
|
||||
/// as text color (source_icon.dart). If we decide later
|
||||
/// to use a generic color as background the following
|
||||
/// line can be used:
|
||||
/// color: Constants.onSecondary,
|
||||
/// as text color (source_icon.dart).
|
||||
color: _sourceTypeValues[index].fgColor,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -66,6 +66,8 @@ class AddSourceForm 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,
|
||||
),
|
||||
|
||||
@@ -14,10 +14,12 @@ class SourceListItem extends StatefulWidget {
|
||||
const SourceListItem({
|
||||
super.key,
|
||||
required this.columnId,
|
||||
required this.sourceIndex,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
final String columnId;
|
||||
final int sourceIndex;
|
||||
final FDSource source;
|
||||
@override
|
||||
State<SourceListItem> createState() => _SourceListItemState();
|
||||
@@ -29,23 +31,22 @@ class _SourceListItemState extends State<SourceListItem> {
|
||||
/// [_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 Source',
|
||||
),
|
||||
title: const Text('Delete Source'),
|
||||
content: const Text(
|
||||
'Do you really want to delete this source? This can not be undone and will also delete all items and bookmarks related to this source.',
|
||||
),
|
||||
@@ -108,64 +109,63 @@ class _SourceListItemState extends State<SourceListItem> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Constants.secondary,
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: Constants.spacingSmall,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
Constants.spacingMiddle,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Characters(widget.source.title)
|
||||
.replaceAll(Characters(''), Characters('\u{200B}'))
|
||||
.toString(),
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
return ReorderableDragStartListener(
|
||||
index: widget.sourceIndex,
|
||||
child: Card(
|
||||
color: Constants.secondary,
|
||||
margin: const EdgeInsets.only(bottom: Constants.spacingSmall),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Characters(widget.source.title)
|
||||
.replaceAll(
|
||||
Characters(''),
|
||||
Characters('\u{200B}'),
|
||||
)
|
||||
.toString(),
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
Characters(
|
||||
widget.source.type.toLocalizedString(),
|
||||
)
|
||||
.replaceAll(Characters(''), Characters('\u{200B}'))
|
||||
.toString(),
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 10.0,
|
||||
Text(
|
||||
Characters(widget.source.type.toLocalizedString())
|
||||
.replaceAll(
|
||||
Characters(''),
|
||||
Characters('\u{200B}'),
|
||||
)
|
||||
.toString(),
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _showDeleteDialog(),
|
||||
icon: _isLoading
|
||||
? const ElevatedButtonProgressIndicator()
|
||||
: const Icon(
|
||||
Icons.delete,
|
||||
),
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
onPressed: () => _showDeleteDialog(),
|
||||
icon: _isLoading
|
||||
? const ElevatedButtonProgressIndicator()
|
||||
: const Icon(Icons.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>MIT</project_license>
|
||||
<name>FeedDeck</name>
|
||||
<summary>Feed reader</summary>
|
||||
<summary>Rss and social media reader</summary>
|
||||
<developer_name>Rico Berger</developer_name>
|
||||
|
||||
<url type="homepage">https://feeddeck.app</url>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 4096 4096" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="icon-linux">
|
||||
<g id="bg" transform="matrix(2.30943,0,0,2.25704,-1582.56,-1114.43)">
|
||||
<path d="M2339.78,621.098C2339.78,619.166 2338.24,617.598 2336.35,617.598L809.714,617.598C807.826,617.598 806.293,619.166 806.293,621.098L806.293,2183.17C806.293,2185.11 807.826,2186.67 809.714,2186.67L2336.35,2186.67C2338.24,2186.67 2339.78,2185.11 2339.78,2183.17L2339.78,621.098Z" style="fill:rgb(73,211,180);"/>
|
||||
</g>
|
||||
<g id="fg" transform="matrix(13.5357,0,0,13.0255,-2722.19,-2672.07)">
|
||||
<path d="M443.567,272.226C438.383,276.258 434.063,278.274 430.607,278.274C423.695,278.274 416.783,274.146 409.871,265.89L369.263,293.826C362.927,298.434 359.759,304.194 359.759,311.106L359.759,429.186L359.471,429.474C359.471,432.354 358.559,435.234 356.735,438.114C354.911,440.994 352.943,442.434 350.831,442.434L350.255,442.434L350.255,439.554L347.951,442.722C345.071,442.53 337.583,439.17 325.487,432.642L325.487,432.93C313.199,425.826 302.927,422.274 294.671,422.274C286.223,422.274 278.447,425.442 271.343,431.778C268.079,434.658 265.439,438.162 263.423,442.29C261.407,446.418 260.111,451.266 259.535,456.834C263.759,451.842 268.943,449.058 275.087,448.482C276.239,448.482 277.007,448.386 277.391,448.194L277.967,448.194C282.191,448.194 290.351,450.594 302.447,455.394C308.207,457.698 315.215,458.85 323.471,458.85C334.991,458.85 347.471,455.778 360.911,449.634C376.271,442.53 385.871,434.082 389.711,424.29C390.287,422.946 390.575,421.602 390.575,420.258L390.863,370.722L410.735,370.722L410.735,431.778L421.391,431.778L421.391,301.314C425.231,300.354 428.687,298.482 431.759,295.698C434.831,292.914 437.807,289.314 440.687,284.898C443.183,281.058 444.431,278.274 444.431,276.546L444.719,276.258C444.719,275.49 444.911,274.53 445.295,273.378L443.567,272.226ZM356.015,297.282C350.447,298.242 345.503,299.874 341.183,302.178C336.863,304.482 332.975,307.266 329.519,310.53C321.647,317.634 317.711,326.562 317.711,337.314L317.711,338.754L317.999,339.042L317.999,340.194C317.231,340.194 316.655,340.098 316.271,339.906L313.679,339.906C298.127,339.906 290.351,347.874 290.351,363.81C290.351,368.418 291.695,372.498 294.383,376.05C297.071,379.602 300.719,381.378 305.327,381.378L305.327,379.362L305.039,379.074L305.039,377.922L304.751,377.634C304.751,373.218 308.015,371.01 314.543,371.01L316.559,371.01L317.135,371.298L318.287,371.298L318.287,413.634C318.095,414.018 317.951,414.498 317.855,415.074C317.759,415.65 317.615,416.13 317.423,416.514C317.039,417.282 316.703,418.194 316.415,419.25C316.127,420.306 315.887,421.026 315.695,421.41L318.575,421.41L319.151,421.122L319.727,421.122L320.015,420.834C322.703,420.834 326.351,419.202 330.959,415.938C344.399,407.106 351.119,396.93 351.119,385.41L351.119,310.818C351.119,308.706 351.647,306.45 352.703,304.05C353.759,301.65 355.055,300.162 356.591,299.586L356.015,297.282ZM375.023,266.754C370.415,270.978 365.231,273.09 359.471,273.09C351.407,273.09 345.647,272.226 342.191,270.498L341.615,270.498L341.327,270.21C338.063,269.634 335.375,269.106 333.263,268.626C331.151,268.146 329.615,267.81 328.655,267.618L325.775,267.618C320.207,267.042 317.231,266.562 316.847,266.178L314.831,267.042C298.319,267.426 286.799,273.186 280.271,284.322C277.391,289.314 275.951,293.73 275.951,297.57L275.951,300.162C282.671,295.938 286.991,293.826 288.911,293.826L290.063,293.826L290.351,293.538C294.767,293.538 302.063,294.594 312.239,296.706C321.839,298.818 329.039,299.874 333.839,299.874L334.127,300.162L335.279,300.162C344.687,300.162 353.615,297.09 362.063,290.946C371.087,284.418 376.079,276.642 377.039,267.618L375.023,266.754ZM409.583,301.89C409.775,301.89 409.919,301.938 410.015,302.034C410.111,302.13 410.351,302.082 410.735,301.89L410.735,340.482L390.863,340.482L390.863,294.402C397.967,299.394 404.207,301.89 409.583,301.89Z" style="fill:rgb(31,34,41);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<svg width="100%" height="100%" viewBox="0 0 129 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="bg" transform="matrix(1,0,0,1,-122,-524)">
|
||||
<path d="M237.505,544.419C237.505,540.046 233.954,536.495 229.581,536.495L142.419,536.495C138.046,536.495 134.495,540.046 134.495,544.419L134.495,631.581C134.495,635.954 138.046,639.505 142.419,639.505L229.581,639.505C233.954,639.505 237.505,635.954 237.505,631.581L237.505,544.419Z" style="fill:rgb(73,211,180);"/>
|
||||
</g>
|
||||
<g id="fg" transform="matrix(0.348583,0,0,0.335444,-58.1233,-58.1329)">
|
||||
<path d="M443.567,272.226C438.383,276.258 434.063,278.274 430.607,278.274C423.695,278.274 416.783,274.146 409.871,265.89L369.263,293.826C362.927,298.434 359.759,304.194 359.759,311.106L359.759,429.186L359.471,429.474C359.471,432.354 358.559,435.234 356.735,438.114C354.911,440.994 352.943,442.434 350.831,442.434L350.255,442.434L350.255,439.554L347.951,442.722C345.071,442.53 337.583,439.17 325.487,432.642L325.487,432.93C313.199,425.826 302.927,422.274 294.671,422.274C286.223,422.274 278.447,425.442 271.343,431.778C268.079,434.658 265.439,438.162 263.423,442.29C261.407,446.418 260.111,451.266 259.535,456.834C263.759,451.842 268.943,449.058 275.087,448.482C276.239,448.482 277.007,448.386 277.391,448.194L277.967,448.194C282.191,448.194 290.351,450.594 302.447,455.394C308.207,457.698 315.215,458.85 323.471,458.85C334.991,458.85 347.471,455.778 360.911,449.634C376.271,442.53 385.871,434.082 389.711,424.29C390.287,422.946 390.575,421.602 390.575,420.258L390.863,370.722L410.735,370.722L410.735,431.778L421.391,431.778L421.391,301.314C425.231,300.354 428.687,298.482 431.759,295.698C434.831,292.914 437.807,289.314 440.687,284.898C443.183,281.058 444.431,278.274 444.431,276.546L444.719,276.258C444.719,275.49 444.911,274.53 445.295,273.378L443.567,272.226ZM356.015,297.282C350.447,298.242 345.503,299.874 341.183,302.178C336.863,304.482 332.975,307.266 329.519,310.53C321.647,317.634 317.711,326.562 317.711,337.314L317.711,338.754L317.999,339.042L317.999,340.194C317.231,340.194 316.655,340.098 316.271,339.906L313.679,339.906C298.127,339.906 290.351,347.874 290.351,363.81C290.351,368.418 291.695,372.498 294.383,376.05C297.071,379.602 300.719,381.378 305.327,381.378L305.327,379.362L305.039,379.074L305.039,377.922L304.751,377.634C304.751,373.218 308.015,371.01 314.543,371.01L316.559,371.01L317.135,371.298L318.287,371.298L318.287,413.634C318.095,414.018 317.951,414.498 317.855,415.074C317.759,415.65 317.615,416.13 317.423,416.514C317.039,417.282 316.703,418.194 316.415,419.25C316.127,420.306 315.887,421.026 315.695,421.41L318.575,421.41L319.151,421.122L319.727,421.122L320.015,420.834C322.703,420.834 326.351,419.202 330.959,415.938C344.399,407.106 351.119,396.93 351.119,385.41L351.119,310.818C351.119,308.706 351.647,306.45 352.703,304.05C353.759,301.65 355.055,300.162 356.591,299.586L356.015,297.282ZM375.023,266.754C370.415,270.978 365.231,273.09 359.471,273.09C351.407,273.09 345.647,272.226 342.191,270.498L341.615,270.498L341.327,270.21C338.063,269.634 335.375,269.106 333.263,268.626C331.151,268.146 329.615,267.81 328.655,267.618L325.775,267.618C320.207,267.042 317.231,266.562 316.847,266.178L314.831,267.042C298.319,267.426 286.799,273.186 280.271,284.322C277.391,289.314 275.951,293.73 275.951,297.57L275.951,300.162C282.671,295.938 286.991,293.826 288.911,293.826L290.063,293.826L290.351,293.538C294.767,293.538 302.063,294.594 312.239,296.706C321.839,298.818 329.039,299.874 333.839,299.874L334.127,300.162L335.279,300.162C344.687,300.162 353.615,297.09 362.063,290.946C371.087,284.418 376.079,276.642 377.039,267.618L375.023,266.754ZM409.583,301.89C409.775,301.89 409.919,301.938 410.015,302.034C410.111,302.13 410.351,302.082 410.735,301.89L410.735,340.482L390.863,340.482L390.863,294.402C397.967,299.394 404.207,301.89 409.583,301.89Z" style="fill:rgb(31,34,41);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -9,8 +9,9 @@
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <volume_controller/volume_controller_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@@ -23,12 +24,15 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
|
||||
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
|
||||
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
||||
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
|
||||
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
|
||||
@@ -6,13 +6,13 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
gtk
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
screen_retriever
|
||||
screen_retriever_linux
|
||||
url_launcher_linux
|
||||
volume_controller
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
media_kit_native_event_loop
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
@@ -8,18 +8,19 @@ import Foundation
|
||||
import app_links
|
||||
import audio_service
|
||||
import audio_session
|
||||
import file_picker
|
||||
import just_audio
|
||||
import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import purchases_flutter
|
||||
import screen_brightness_macos
|
||||
import screen_retriever
|
||||
import screen_retriever_macos
|
||||
import shared_preferences_foundation
|
||||
import sign_in_with_apple
|
||||
import sqflite
|
||||
import url_launcher_macos
|
||||
import volume_controller
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
@@ -27,18 +28,19 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin"))
|
||||
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
|
||||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
||||
@@ -1,69 +1,34 @@
|
||||
PODS:
|
||||
- app_links (1.0.0):
|
||||
- FlutterMacOS
|
||||
- audio_service (0.14.1):
|
||||
- FlutterMacOS
|
||||
- audio_session (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- just_audio (0.0.1):
|
||||
- FlutterMacOS
|
||||
- media_kit_libs_macos_video (1.0.4):
|
||||
- FlutterMacOS
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- FlutterMacOS
|
||||
- media_kit_video (0.0.1):
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.0.1):
|
||||
- purchases_flutter (9.1.0):
|
||||
- FlutterMacOS
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- purchases_flutter (6.30.2):
|
||||
- FlutterMacOS
|
||||
- PurchasesHybridCommon (= 11.1.0)
|
||||
- PurchasesHybridCommon (11.1.0):
|
||||
- RevenueCat (= 4.43.2)
|
||||
- RevenueCat (4.43.2)
|
||||
- screen_brightness_macos (0.1.0):
|
||||
- FlutterMacOS
|
||||
- screen_retriever (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- PurchasesHybridCommon (= 16.0.2)
|
||||
- PurchasesHybridCommon (16.0.2):
|
||||
- RevenueCat (= 5.33.1)
|
||||
- RevenueCat (5.33.1)
|
||||
- screen_retriever_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- sign_in_with_apple (0.0.1):
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- window_manager (0.2.0):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
||||
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`)
|
||||
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
|
||||
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- purchases_flutter (from `Flutter/ephemeral/.symlinks/plugins/purchases_flutter/macos`)
|
||||
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
|
||||
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||
- sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -71,68 +36,35 @@ SPEC REPOS:
|
||||
- RevenueCat
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
||||
audio_service:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/macos
|
||||
audio_session:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
just_audio:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/macos
|
||||
media_kit_libs_macos_video:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
||||
media_kit_native_event_loop:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
|
||||
media_kit_video:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
purchases_flutter:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/purchases_flutter/macos
|
||||
screen_brightness_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
|
||||
screen_retriever:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
screen_retriever_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
|
||||
sign_in_with_apple:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
window_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
|
||||
audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9
|
||||
audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
purchases_flutter: 3407100959d2aeb636507b2c98970d850dae57ae
|
||||
PurchasesHybridCommon: 4022d5944cb30ec44ba5159e42aa161fe0e30175
|
||||
RevenueCat: 3d934653b7e8b09af88fd47e9e84cfaf5d0a89ba
|
||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sign_in_with_apple: a9e97e744e8edc36aefc2723111f652102a7a727
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
||||
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
|
||||
purchases_flutter: 7655c5b1ec1236b102c30f02d3d0c7d87117bd2d
|
||||
PurchasesHybridCommon: ae7a0a6e105ecdde3e8816a004e57f0a2a7b9261
|
||||
RevenueCat: b0ed01125b05a45b8264a2951ad68acb61942038
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
|
||||
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
|
||||
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
|
||||
|
||||
PODFILE CHECKSUM: 8d40c19d3cbdb380d870685c3a564c989f1efa52
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
553BE2622AC0648A002EA0C0 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 553BE2612AC0648A002EA0C0 /* StoreKit.framework */; };
|
||||
FBF50439F2C4D0BF615EFED2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27D60858A3CD81E41E2CDBF2 /* Pods_Runner.framework */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -83,6 +84,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
FBF50439F2C4D0BF615EFED2 /* Pods_Runner.framework in Frameworks */,
|
||||
553BE2622AC0648A002EA0C0 /* StoreKit.framework in Frameworks */,
|
||||
);
|
||||
@@ -180,6 +182,9 @@
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
33CC10EC2044A3C60003C045 /* Runner */ = {
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
@@ -205,6 +210,9 @@
|
||||
|
||||
/* Begin PBXProject section */
|
||||
33CC10E52044A3C60003C045 /* Project object */ = {
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
@@ -645,6 +653,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 = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = ""$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "FeedDeck.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -48,6 +66,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
@NSApplicationMain
|
||||
@main
|
||||
class AppDelegate: FlutterAppDelegate {
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
|
||||