Compare commits

..

90 Commits

Author SHA1 Message Date
semantic-release-bot
1997e474f2 chore(release): 1.0.0-dev.1 [skip ci]
# [1.0.0-dev.1](https://github.com/revanced/revanced-manager/compare/v0.0.57...v1.0.0-dev.1) (2023-04-29)

### Bug Fixes

* fix broken manager update implmentation ([6492507](6492507d0e))
* **i18n:** update translation for `refreshSucess` ([b286444](b286444ad9))
* open contributor links externally ([#791](https://github.com/revanced/revanced-manager/issues/791)) ([f0b0282](f0b028279c))
* resized monochrome icon to match the original ([#789](https://github.com/revanced/revanced-manager/issues/789)) ([ac830cb](ac830cbe7f))
* update pubspec version ([65da6af](65da6af3f9))

* chore!: merge `dev` to `main` (#830) ([782c012](782c0127b1)), closes [#830](https://github.com/revanced/revanced-manager/issues/830)

### Features

* add continue anyway button to select from storage dialog ([#810](https://github.com/revanced/revanced-manager/issues/810)) ([affba66](affba669ce))
* add option to import/export keystore ([#776](https://github.com/revanced/revanced-manager/issues/776)) ([dca2d4f](dca2d4fe12))
* appreciation message for new contributors ([0a1f2da](0a1f2da33d))
* auto select default patches ([4c9cb56](4c9cb560e3))
* change `continue anyways` to `cancel` ([cfc866b](cfc866bef2))
* **ci:** update crowdin workflow to use new main branch ([ded59d2](ded59d2da0))
* confirmation dialog for deleting keystore ([#764](https://github.com/revanced/revanced-manager/issues/764)) ([054afbb](054afbbedd))
* disable selecting installed apps for nonroot ([bb681e3](bb681e31c9))
* improve suggested app version text ([#822](https://github.com/revanced/revanced-manager/issues/822)) ([ad17995](ad17995f28))
* improve ux ([#752](https://github.com/revanced/revanced-manager/issues/752)) ([3b677f8](3b677f8ae3)), closes [#782](https://github.com/revanced/revanced-manager/issues/782)
* progress bar for manager updates ([f5aafdb](f5aafdb7d6))
* remove notice about stale development [skip ci] ([62f7a82](62f7a820d8))
* resetting source to default dismiss the sources pop-up ([#797](https://github.com/revanced/revanced-manager/issues/797)) ([62ef1c8](62ef1c88fe))
* trim extra space when setting custom source ([#771](https://github.com/revanced/revanced-manager/issues/771)) ([37b583f](37b583f560))
* warning for armv7 devices ([cdfb09f](cdfb09fbfa))

### BREAKING CHANGES

* this is to release as 1.0.0
2023-04-29 19:19:25 +00:00
Aunali321
6492507d0e fix: fix broken manager update implmentation 2023-04-30 00:39:42 +05:30
semantic-release-bot
3091f441ea chore(release): 1.0.0 [skip ci]
# [1.0.0](https://github.com/revanced/revanced-manager/compare/v0.0.57...v1.0.0) (2023-04-29)

### Bug Fixes

* **i18n:** update translation for `refreshSucess` ([b286444](b286444ad9))
* open contributor links externally ([#791](https://github.com/revanced/revanced-manager/issues/791)) ([f0b0282](f0b028279c))
* resized monochrome icon to match the original ([#789](https://github.com/revanced/revanced-manager/issues/789)) ([ac830cb](ac830cbe7f))
* update pubspec version ([65da6af](65da6af3f9))

* chore!: merge `dev` to `main` (#830) ([782c012](782c0127b1)), closes [#830](https://github.com/revanced/revanced-manager/issues/830)

### Features

* add continue anyway button to select from storage dialog ([#810](https://github.com/revanced/revanced-manager/issues/810)) ([affba66](affba669ce))
* add option to import/export keystore ([#776](https://github.com/revanced/revanced-manager/issues/776)) ([dca2d4f](dca2d4fe12))
* appreciation message for new contributors ([0a1f2da](0a1f2da33d))
* auto select default patches ([4c9cb56](4c9cb560e3))
* change `continue anyways` to `cancel` ([cfc866b](cfc866bef2))
* **ci:** update crowdin workflow to use new main branch ([ded59d2](ded59d2da0))
* confirmation dialog for deleting keystore ([#764](https://github.com/revanced/revanced-manager/issues/764)) ([054afbb](054afbbedd))
* disable selecting installed apps for nonroot ([bb681e3](bb681e31c9))
* improve suggested app version text ([#822](https://github.com/revanced/revanced-manager/issues/822)) ([ad17995](ad17995f28))
* improve ux ([#752](https://github.com/revanced/revanced-manager/issues/752)) ([3b677f8](3b677f8ae3)), closes [#782](https://github.com/revanced/revanced-manager/issues/782)
* progress bar for manager updates ([f5aafdb](f5aafdb7d6))
* remove notice about stale development [skip ci] ([62f7a82](62f7a820d8))
* resetting source to default dismiss the sources pop-up ([#797](https://github.com/revanced/revanced-manager/issues/797)) ([62ef1c8](62ef1c88fe))
* trim extra space when setting custom source ([#771](https://github.com/revanced/revanced-manager/issues/771)) ([37b583f](37b583f560))
* warning for armv7 devices ([cdfb09f](cdfb09fbfa))

### BREAKING CHANGES

* this is to release as 1.0.0
2023-04-29 18:16:26 +00:00
Palm
782c0127b1 chore!: merge dev to main (#830)
BREAKING CHANGE: this is to release as 1.0.0
2023-04-30 01:06:26 +07:00
Palm
d2e8e7dd5d chore: comply with review changes 2023-04-30 01:03:36 +07:00
Palm
ce12ec89c4 ci(release): automatically bump version codes 2023-04-29 20:53:54 +07:00
EvadeMaster
b286444ad9 fix(i18n): update translation for refreshSucess 2023-04-29 20:43:02 +07:00
EvadeMaster
941263102d refactor: change name from "recommendedVersion" to "suggestedVersion" 2023-04-29 20:43:02 +07:00
Palm
e2ed296dc7 chore(deps): constrain get_it to 7.2.0 2023-04-29 19:46:07 +07:00
Ushie
cfc866bef2 feat: change continue anyways to cancel 2023-04-29 15:37:30 +03:00
Sebok Andras
affba669ce feat: add continue anyway button to select from storage dialog (#810) 2023-04-29 15:36:25 +03:00
Palm
7230152ab8 chore: fix issues with gitignore [skip ci] 2023-04-29 17:42:19 +07:00
Palm
21fee7171f chore: update pubspec version
skip-checks: true
2023-04-29 15:40:25 +07:00
SodaWithoutSparkles
ad17995f28 feat: improve suggested app version text (#822) 2023-04-28 17:41:28 +03:00
Yaros
ac830cbe7f fix: resized monochrome icon to match the original (#789)
cherry-pick from 6281ae82876c69ff4194a782e4b08b398a1285d6
2023-04-27 18:12:48 +07:00
Aunali321
65da6af3f9 fix: update pubspec version 2023-04-21 14:56:31 +03:00
oSumAtrIX
3d90bf7588 ci: bypass push permission on protected branches with owner PAT 2023-04-21 14:55:29 +03:00
EvadeMaster
62ef1c88fe feat: resetting source to default dismiss the sources pop-up (#797)
* feat: resetting source to default dismiss the sources pop-up

* chore: format using `dart format`
2023-04-21 00:29:37 +05:30
Aunali321
d6918920b6 chore: add missing translations 2023-04-20 23:53:24 +05:30
Aunali321
cdfb09fbfa feat: warning for armv7 devices 2023-04-20 23:20:30 +05:30
Aunali321
bb681e31c9 feat: disable selecting installed apps for nonroot 2023-04-20 22:25:29 +05:30
Aunali321
c7483936ec docs: remove alpha disclaimer 2023-04-20 05:07:46 +05:30
Aunali321
0a1f2da33d feat: appreciation message for new contributors 2023-04-20 05:04:37 +05:30
Aunali321
f5aafdb7d6 feat: progress bar for manager updates 2023-04-20 04:45:46 +05:30
Aunali321
c9adf1c492 style: sort imports 2023-04-19 01:32:43 +05:30
Aunali321
4c9cb560e3 feat: auto select default patches 2023-04-19 01:21:08 +05:30
Yaros
f0b028279c fix: open contributor links externally (#791) 2023-04-18 19:46:16 +05:30
Aman Sikarwar
197770b68b chore: update dependencies (#772)
* chore: updated some dependencies

* refactor: reimplemented cache interceptor

* Revert "Updated dependencies & migrated breaking changes"

This reverts commit e6743b0d6b2552fdbf1c99d23e158e682362dd5d.

* chore: migrated flutter_local_notifications

* revert: reimplemented cache interceptor
2023-04-18 19:45:29 +05:30
EvadeMaster
37b583f560 feat: trim extra space when setting custom source (#771) 2023-04-18 17:15:04 +07:00
Sebok Andras
dca2d4fe12 feat: add option to import/export keystore (#776)
* feat: add option to import/export keystore

* change the order of import/export keystore buttons

* feat: add option to change the keystore password
2023-04-18 15:08:10 +05:30
Aunali321
3b677f8ae3 feat: improve ux (#752)
* feat: restart app toast when changing sources, api url

* fix: potentially fix manager stuck on black screen

* feat: remove select all patches chip

* feat: show all apps and recommended versions

* chore(i18n): remove unused strings

remove unused strings left out in 7e05bca

* feat: select install type before patching

* feat: update patches button (#782)

* feat: update patches button

* feat: show toast when force refresh

* chore: don't translate "ReVanced Manager" and "ReVanced Patches"

* Revert "feat: select install type before patching"

This reverts commit 74e0c09b54.

* feat: rename recommended patches to default patches

* feat: add missing localization

* feat: display restart app toast for resetting source

---------

Co-authored-by: EvadeMaster <93124920+EvadeMaster@users.noreply.github.com>
2023-04-18 13:27:26 +05:30
Palm
0b952578d1 ci(release): update semantic-release 2023-04-02 02:37:49 +07:00
EvadeMaster
054afbbedd feat: confirmation dialog for deleting keystore (#764)
* feat: confirmation dialog for deleting keystore

* refactor(i18n): apply suggestion from code-reviewer

Co-authored-by: Ushie <github@ushie.dev>

* refactor: apply suggestion from code-reviewer

Co-authored-by: Mipirakas <borismichiels@gmail.com>

---------

Co-authored-by: Ushie <github@ushie.dev>
Co-authored-by: Mipirakas <borismichiels@gmail.com>
2023-04-01 21:02:28 +07:00
Palm
866a6e4a44 chore(CHANGELOG): reset changelog 2023-03-30 23:29:26 +07:00
Palm
8ea7dd478b ci(release): update node dependencies 2023-03-30 23:20:31 +07:00
EvadeMaster
7839252934 ci(analyze): only run when necessary (#766)
* ci(analyser): only run on lib/`.dart` changes

* ci(analyser): runs on workflow changes

* ci(analyze): run on all dart file changes

---------

Co-authored-by: Palm <palmpasuthorn@gmail.com>
2023-03-29 20:51:08 +03:00
EvadeMaster
fa4063220f ci: dart analyser (#761)
* ci: dart analyser

* ci(analyser): clarify the dart analysing step

* refactor: ignore generated files

* ci(analyser): apply suggestion from code-reviewer

Co-Authored-By: Palm <palmpasuthorn@gmail.com>

* ci(analyser): apply suggestion from code-reviewer

Co-Authored-By: Palm <palmpasuthorn@gmail.com>

* ci(analyser): apply suggestion from code-reviewer

Co-authored-by: Palm <palmpasuthorn@gmail.com>

---------

Co-authored-by: Palm <palmpasuthorn@gmail.com>
2023-03-28 15:47:34 +07:00
Palm
d214a02abd ci(release): fix argument parsing of npm exec 2023-03-26 23:02:57 +07:00
Palm
d1c12edd1b ci(release): use appropriate npm commands for ci environment 2023-03-26 23:02:57 +07:00
Palm
ded1a44c37 ci(release): fix step not moving artifact to accessible path 2023-03-26 23:02:57 +07:00
Ushie
790a6cd1e3 ci(release): remove unnecessary variables in flutter build step 2023-03-26 18:57:27 +03:00
EvadeMaster
a79f883a0f ci: use caching to speed up workflows (#760)
Use caching to speed up workflows: 9-14 minutes (ish) to 6-8 minutes (ish)

Commits:
* ci(release): use GitHub cache

* ci(release): restore signing
2023-03-26 20:02:56 +07:00
Palm
d9c5a540a3 ci(release): fix wrong artifact name and ci not uploading build 2023-03-24 01:01:44 +07:00
Palm
276f33b9ec ci: use semantic-release (#746) 2023-03-23 17:34:52 +01:00
Ushie
ded59d2da0 feat(ci): update crowdin workflow to use new main branch 2023-03-20 22:39:29 +03:00
Ushie
62f7a820d8 feat: remove notice about stale development [skip ci] 2023-03-20 18:03:58 +03:00
Ushie
7063ffa013 build: bump version to v0.0.57 2023-03-14 21:00:48 +03:00
Ushie
bf4dc3c095 chore: bump stacked
stacked: ^3.0.0 -> ^3.2.0
stacked_generator: ^0.8.0 -> ^1.0.0
stacked_services: ^0.9.3 -> ^1.0.0
2023-03-14 21:00:28 +03:00
EvadeMaster
c10e5848bf Merge pull request #707 from revanced/dev
chore: merge branch `dev` to `flutter`
2023-03-14 22:07:35 +07:00
EvadeMaster
92a3b0d6e0 feat(style): use native switch & chip (#732)
* chore: remove useless themedata

* feat(style): new switch

* feat(style): use native chip components

* chore: remove unused import

* feat(accessibility): set tooltip

* chore: remove unneeded themedata

* chore: fix theme

* feat(i18n): add 3 new strings

* feat(style): correct material 3 theme on nondynamic
2023-03-14 21:53:42 +07:00
Aunali321
b475bd25c8 build: remove env setup step 2023-03-11 23:01:26 +05:30
Aunali321
d318224a6f fix: black screen after resetting custom sources 2023-03-06 18:08:48 +05:30
Aunali321
0074fee865 build: new ci (#731)
New Build CI

Commits:
* buid: re-do ci

* build: ignore tags

* build: get the latest flutter version automatically && formatting

---------

Co-authored-by: EvadeMaster <93124920+EvadeMaster@users.noreply.github.com>
2023-03-05 16:23:51 +07:00
Aunali321
5617535a63 refactor: remove sentry and crowdin (#730)
We no longer use sentry and crowdin.
2023-03-05 16:12:46 +07:00
EvadeMaster
68ccefc59f revert: "refactor: update deprecated and minor code refactors (#710)"
This reverts commit 6829d3cdea.

Signed-off-by: EvadeMaster <93124920+EvadeMaster@users.noreply.github.com>
2023-03-03 18:06:24 +07:00
Ushie
6d60541626 chore(deps): meet patcher breaking changes 2023-03-03 03:11:44 +03:00
EvadeMaster
a635e5b8d0 chore: addFiles -> addIntegrations (#725)
fix #721
2023-03-02 14:51:35 +07:00
EvadeMaster
48a10440fe ci(build): remove environment on PR build 2023-03-02 11:06:15 +07:00
EvadeMaster
8e3ba88318 chore: bump gradle 7.5-rc.1 -> 7.6.1 (#717)
* chore: upgrade gradle from 7.5-rc.1 -> 7.6.1

* chore: remove unrelated changes
2023-02-27 19:26:10 +07:00
EvadeMaster
ab8fccc544 chore(deps): bump patcher version from 6.4.3 -> 7.0.0 2023-02-27 19:19:19 +07:00
EvadeMaster
8319dc9164 fix: improperly sized monochrome icon (#715) 2023-02-27 19:07:44 +07:00
Sailesh Dahal
6829d3cdea refactor: update deprecated and minor code refactors (#710)
Improve code readability & additional refactoring

Commits:
chore: exclude generated from analyzer

refactor: add SharedPreferences to locator

refactor: access shared pref from locator, and code refactor

refactor: remove unwanted `await`

refactor: remove `const` from `CacheConfig`
2023-02-27 19:06:05 +07:00
EvadeMaster
3ae4d69110 chore: migrate deprecation code && code cleanup (#708)
Fixes all issues in `flutter analyze`.
<Reviewed>

Commits:
* chore: migrate deprecated text style

* chore: migrate `toggleableActiveColor` to individual theme

* chore: don't use 'BuildContext's across async gaps
2023-02-20 16:53:53 +07:00
LisoUseInAIKyrios
dc665f227e fix: use high resolution adaptive icons (#675)
Use high-resolution icons instead of the low-resolution ones on ReVanced Manager.

Commits:
* High resolution adaptive icons, built using revanced-logo-no-background.svg

Same icons currently used for ReVanced YouTube.

* tweak background color to match revanced-logo-no-background.svg

* recreated foreground using 'revanced-logo-shape.svg'

* updated full resolution icons, using same ratio as original SVG

* updated icons with gradient border
2023-02-17 19:28:01 +07:00
EvadeMaster
a83496568f chore: code cleanup (#681)
Improve the readability of the code.

Commits:
* chore: correct typos `fragement` to `fragment`

* chore: put a single newline at end of file.
2023-02-17 18:53:23 +07:00
Alex
12d25570af fix: Long patch description truncated (#702)
fix: long patch description truncated (#702)

Allow flexible height
2023-02-14 18:42:24 +07:00
EvadeMaster
378c947654 chore: bump version to v0.0.56 2023-02-12 13:04:34 +07:00
EvadeMaster
bd39a3140e feat: fix patch bundle version no longer displayed (#686) 2023-02-12 00:02:30 +07:00
Boris Michiels
7d3ca3dec1 fix: display patches version on first load (#687) 2023-02-12 00:00:01 +07:00
oSumAtrIX
1cb556c8f8 chore: bump version to 0.0.55 2023-02-11 23:59:53 +07:00
oSumAtrIX
8c8f96de1c build: bump patcher version 2023-02-11 23:59:47 +07:00
EvadeMaster
318cd87a9a feat(style): use the correct m3 theming (partially) (#680) 2023-02-07 15:46:29 +03:00
Aunali321
5d63d5c2d3 feat: potentially fix apps disappearing when update is available (#674) 2023-01-31 15:42:00 +03:00
Ushie
7d347fccc6 build: bump version to v0.0.54 2023-01-30 15:57:06 +03:00
Jay Gajjar
a54ca799b9 feat: update rules of analysis_options.yaml. and solved all problems (#665)
* update rules of analysis_options.yaml. and solved all problems

* refactor: fix remaining problems

---------

Co-authored-by: Ushie <ushiekane@gmail.com>
2023-01-30 15:35:06 +03:00
Mipirakas
f5bc1a996f feat: remove select all icon from searchbar (#669) 2023-01-30 15:13:25 +03:00
Mipirakas
8591bc4d01 chore: update cr_file_saver (#668)
Co-authored-by: Ushie <github@ushie.dev>
2023-01-30 15:11:31 +03:00
EvadeMaster
40888c07f3 feat: fix overflow text & separate version from patch name (#666)
* chore: Add note

* feat: Fix overflow text & seperate version from patch title

* feat: Fix overflow text & separate version from patch name
2023-01-30 15:07:25 +03:00
Mipirakas
1c965c3788 refactor: cleanup remember patches feature (#630) 2023-01-30 15:03:55 +03:00
Bennett
4df690c2a2 feat: monochrome icon + predictive back gesture (#591)
* Add monochrome icon

* Enable predictive back gesture

* feat: monochrome icon + predictive back gesture
2023-01-30 01:23:56 +03:00
Abhiram
d6abb61e2b ci(build): update workflow actions (#662)
- Bump upload-artifact to v3

- Update crowdin/github-action according to new versioning format [https://github.com/crowdin/github-action/releases/tag/v1.6.0]
2023-01-29 23:56:13 +03:00
oSumAtrIX
3434c862e9 feat: improve note [skip ci] 2023-01-28 07:49:57 +01:00
EvadeMaster
ea8af926fa chore: update broken documentation link (#659)
* chorus: fix invalid documentation link

* chorus: Update broken documentation link
2023-01-22 16:18:41 +03:00
EvadeMaster
c3df48174c feat: warn user for selecting all patches (#649) 2023-01-21 16:37:28 +01:00
oSumAtrIX
f1e60f96c4 chore: bump version to 0.0.53 2023-01-18 22:07:00 +01:00
oSumAtrIX
cdd852678b build: bump patcher version 2023-01-18 22:07:00 +01:00
Ushie
bf518b5467 bump: kotlin gradle plugin 2023-01-18 22:07:00 +01:00
oSumAtrIX
ffd53fab26 feat: clarify acknowledgement label (#608) 2023-01-14 20:23:49 +03:00
oSumAtrIX
5aad7dad35 chore: fix incorrect wording 2023-01-12 21:03:11 +01:00
oSumAtrIX
b1c1a9f4e1 feat: stale development notice [skip ci] 2023-01-07 14:24:43 +01:00
108 changed files with 9688 additions and 1533 deletions

3
.env
View File

@@ -1,3 +0,0 @@
sentryDSN=
apiKey=
appId=

View File

@@ -116,5 +116,5 @@ body:
required: true required: true
- label: I filled out all of the requested information in this issue properly. - label: I filled out all of the requested information in this issue properly.
required: true required: true
- label: The issue is related solely to the ReVanced Manager - label: The issue is related solely to the ReVanced Manager and not related to patching errors or patches
required: true required: true

2
.github/config.yaml vendored Normal file
View File

@@ -0,0 +1,2 @@
firstPRMergeComment: >
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.

32
.github/workflows/analyze.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Analyze Code
on:
push:
branches: [ "main", "dev" ]
paths:
- "**.dart"
- ".github/workflows/analyze.yml"
pull_request:
branches: [ "main", "dev" ]
paths:
- "**.dart"
- ".github/workflows/analyze.yml"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Install Flutter dependencies
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Analyze code
uses: ValentinVignal/action-dart-analyze@v0.15
with:
fail-on: warning

View File

@@ -1,55 +0,0 @@
name: "Android CI Actions"
on:
push:
branches:
- "**"
tags-ignore:
- "v*" # Ignore tags that start with "v" (e.g. v1.0.0) because they are handled by release-build.yml
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 12
uses: actions/setup-java@v3
with:
java-version: '12'
distribution: 'zulu'
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
- name: Set environment variables
run: echo $SECRETS | base64 -d > lib/utils/environment.dart
env:
SECRETS: ${{ secrets.SECRETS }}
- name: Set up Flutter
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Build with Flutter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
run: flutter build apk
- name: Sign APK
id: sign_apk
uses: ilharp/sign-android-release@v1
with:
releaseDir: build/app/outputs/apk/release
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Add version to APK
run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
- name: Upload APK
uses: actions/upload-artifact@v2
with:
name: revanced-manager-${{ env.RELEASE_VERSION }}
path: revanced-manager-${{ env.RELEASE_VERSION }}.apk

View File

@@ -3,7 +3,7 @@ name: Sync Crowdin translations
on: on:
push: push:
branches: branches:
- "flutter" - "dev"
paths: paths:
- "assets/i18n/en_US.json" - "assets/i18n/en_US.json"
- ".github/workflows/crowdin.yml" - ".github/workflows/crowdin.yml"
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Crowdin - name: Crowdin
uses: crowdin/github-action@1.5.0 uses: crowdin/github-action@v1
with: with:
config: crowdin.yml config: crowdin.yml
upload_sources: true upload_sources: true

View File

@@ -1,41 +1,41 @@
name: "Android CI PR Build" name: PR Build
on: on:
pull_request: workflow_dispatch:
branches:
- "**" jobs:
build:
jobs: name: Build
release: runs-on: ubuntu-latest
runs-on: ubuntu-latest steps:
steps: - name: Checkout
- uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set env with:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # Make sure the release step uses its own credentials:
- name: Set up JDK 12 # https://github.com/cycjimmy/semantic-release-action#private-packages
uses: actions/setup-java@v3 persist-credentials: false
with: fetch-depth: 0
java-version: '12' - name: Setup JDK
distribution: 'zulu' uses: actions/setup-java@v3
- uses: subosito/flutter-action@v2 with:
with: java-version: '17'
channel: 'stable' distribution: 'zulu'
- name: Set environment variables cache: 'gradle'
run: echo $SECRETS | base64 -d > lib/utils/environment.dart - name: Setup Flutter
env: uses: subosito/flutter-action@v2
SECRETS: ${{ secrets.SECRETS }} with:
- name: Set up Flutter channel: 'stable'
run: flutter pub get cache: true
- name: Generate files with Builder - name: Install Flutter dependencies
run: flutter packages pub run build_runner build --delete-conflicting-outputs run: flutter pub get
- name: Build with Flutter - name: Generate files with Builder
env: run: flutter packages pub run build_runner build --delete-conflicting-outputs
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build with Flutter
run: flutter build apk env:
- name: Add version to APK GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: mv build/app/outputs/flutter-apk/app-release.apk revanced-manager-${{ env.RELEASE_VERSION }}.apk run: flutter build apk
- name: Upload APK - name: Upload build
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: revanced-manager-${{ env.RELEASE_VERSION }} name: revanced-manager
path: revanced-manager-${{ env.RELEASE_VERSION }}.apk path: build/app/outputs/flutter-apk/app-release.apk

View File

@@ -1,54 +0,0 @@
name: "Release Build"
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 12
uses: actions/setup-java@v3
with:
java-version: '12'
distribution: 'zulu'
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
- name: Set environment variables
run: echo $SECRETS | base64 -d > lib/utils/environment.dart
env:
SECRETS: ${{ secrets.SECRETS }}
- name: Set up Flutter
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Build with Flutter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
run: flutter build apk
- name: Sign APK
id: sign_apk
uses: ilharp/sign-android-release@v1
with:
releaseDir: build/app/outputs/apk/release
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Add version to APK
run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
- name: Publish release APK
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk

68
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- main
- dev
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'zulu'
cache: 'gradle'
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Install Flutter dependencies
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Build with Flutter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: flutter build apk
- name: Sign APK
id: sign_apk
uses: ilharp/sign-android-release@v1
with:
releaseDir: build/app/outputs/apk/release
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Setup semantic-release
run: npm ci
- name: Get release version
run: npm exec -- semantic-release --dry-run
id: get-next-version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Add version to APK
run: mv $SIGNED_FILE_PATH $(dirname $SIGNED_FILE_PATH)/revanced-manager-${{ steps.get-next-version.outputs.new-release-version }}.apk
env:
SIGNED_FILE_PATH: ${{steps.sign_apk.outputs.signedFile}}
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
run: npm exec semantic-release

7
.gitignore vendored
View File

@@ -134,5 +134,8 @@ app.*.map.json
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock !/dev/ci/**/Gemfile.lock
Firebase related # Firebase related
.firebase .firebase
# Dependency directories
node_modules/

75
.releaserc Normal file
View File

@@ -0,0 +1,75 @@
{
"branches": [
"main",
{
"name": "dev",
"prerelease": true
}
],
"plugins": [
"semantic-release-export-data",
"@semantic-release/commit-analyzer",
[
"@semantic-release/release-notes-generator",
{
"presetConfig": {
"types": [
{
"type": "build",
"section": "Dependency Updates"
},
{
"type": "chore",
"section": "Other Changes",
"hidden": false
},
{
"type": "perf",
"section": "Performance Improvements",
"hidden": false
},
{
"type": "refactor",
"section": "Code Improvements",
"hidden": false
}
]
}
}
],
"@semantic-release/changelog",
"semantic-release-flutter-plugin",
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"pubspec.yaml"
]
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "build/app/outputs/apk/release/revanced-manager-*.apk"
}
],
"successComment": false
}
],
[
"@saithodev/semantic-release-backmerge",
{
"backmergeBranches": [
{
"from": "main",
"to": "dev"
}
],
"clearWorkspace": true
}
]
]
}

74
CHANGELOG.md Normal file
View File

@@ -0,0 +1,74 @@
# [1.0.0-dev.1](https://github.com/revanced/revanced-manager/compare/v0.0.57...v1.0.0-dev.1) (2023-04-29)
### Bug Fixes
* fix broken manager update implmentation ([6492507](https://github.com/revanced/revanced-manager/commit/6492507d0e0015b594f7d9e97bbadee9501b29d6))
* **i18n:** update translation for `refreshSucess` ([b286444](https://github.com/revanced/revanced-manager/commit/b286444ad93fc6009412ac14b996ff6268069811))
* open contributor links externally ([#791](https://github.com/revanced/revanced-manager/issues/791)) ([f0b0282](https://github.com/revanced/revanced-manager/commit/f0b028279c69f97817952063d84809d3e486ad6e))
* resized monochrome icon to match the original ([#789](https://github.com/revanced/revanced-manager/issues/789)) ([ac830cb](https://github.com/revanced/revanced-manager/commit/ac830cbe7f3b1ebd7849e586b829d3c077436a0d))
* update pubspec version ([65da6af](https://github.com/revanced/revanced-manager/commit/65da6af3f96550b138dcaf61832a5de23f529b32))
* chore!: merge `dev` to `main` (#830) ([782c012](https://github.com/revanced/revanced-manager/commit/782c0127b1f6f5380bd8ca23c8c2f280e2bfc432)), closes [#830](https://github.com/revanced/revanced-manager/issues/830)
### Features
* add continue anyway button to select from storage dialog ([#810](https://github.com/revanced/revanced-manager/issues/810)) ([affba66](https://github.com/revanced/revanced-manager/commit/affba669ce1ca6866a1dd1bd801e3f33e4bfe051))
* add option to import/export keystore ([#776](https://github.com/revanced/revanced-manager/issues/776)) ([dca2d4f](https://github.com/revanced/revanced-manager/commit/dca2d4fe126a6966a094d335e0f27bb62d76c5e8))
* appreciation message for new contributors ([0a1f2da](https://github.com/revanced/revanced-manager/commit/0a1f2da33da7d44f0613b19f3e6b2b7b50240548))
* auto select default patches ([4c9cb56](https://github.com/revanced/revanced-manager/commit/4c9cb560e3e38295a5140419f2565b478cb6c497))
* change `continue anyways` to `cancel` ([cfc866b](https://github.com/revanced/revanced-manager/commit/cfc866bef2497bc1675bf5dea834cea59d9cc969))
* **ci:** update crowdin workflow to use new main branch ([ded59d2](https://github.com/revanced/revanced-manager/commit/ded59d2da0d193b2dea4a5a7f2fc8eefaceecc0a))
* confirmation dialog for deleting keystore ([#764](https://github.com/revanced/revanced-manager/issues/764)) ([054afbb](https://github.com/revanced/revanced-manager/commit/054afbbedd70a1933d8241ff5b63a772f90b555f))
* disable selecting installed apps for nonroot ([bb681e3](https://github.com/revanced/revanced-manager/commit/bb681e31c9c4e8a5b7b0c883edd1bc5c28505627))
* improve suggested app version text ([#822](https://github.com/revanced/revanced-manager/issues/822)) ([ad17995](https://github.com/revanced/revanced-manager/commit/ad17995f2883682f67eb42b1f82ca865fba86ef9))
* improve ux ([#752](https://github.com/revanced/revanced-manager/issues/752)) ([3b677f8](https://github.com/revanced/revanced-manager/commit/3b677f8ae3739c079e2116417fef6ed395c2ff06)), closes [#782](https://github.com/revanced/revanced-manager/issues/782)
* progress bar for manager updates ([f5aafdb](https://github.com/revanced/revanced-manager/commit/f5aafdb7d6f51386b667abbccf7f2521ef664ba5))
* remove notice about stale development [skip ci] ([62f7a82](https://github.com/revanced/revanced-manager/commit/62f7a820d8ee2506376306e119698d427de745ef))
* resetting source to default dismiss the sources pop-up ([#797](https://github.com/revanced/revanced-manager/issues/797)) ([62ef1c8](https://github.com/revanced/revanced-manager/commit/62ef1c88fe0352d3962f8c73edff4b99ea347c0f))
* trim extra space when setting custom source ([#771](https://github.com/revanced/revanced-manager/issues/771)) ([37b583f](https://github.com/revanced/revanced-manager/commit/37b583f560910c0b5da2a8e64de9b53f5a26bb56))
* warning for armv7 devices ([cdfb09f](https://github.com/revanced/revanced-manager/commit/cdfb09fbfa8e74d84ddcc91565489c3c5b61dfa2))
### BREAKING CHANGES
* this is to release as 1.0.0
# [1.0.0](https://github.com/revanced/revanced-manager/compare/v0.0.57...v1.0.0) (2023-04-29)
### Bug Fixes
* **i18n:** update translation for `refreshSucess` ([b286444](https://github.com/revanced/revanced-manager/commit/b286444ad93fc6009412ac14b996ff6268069811))
* open contributor links externally ([#791](https://github.com/revanced/revanced-manager/issues/791)) ([f0b0282](https://github.com/revanced/revanced-manager/commit/f0b028279c69f97817952063d84809d3e486ad6e))
* resized monochrome icon to match the original ([#789](https://github.com/revanced/revanced-manager/issues/789)) ([ac830cb](https://github.com/revanced/revanced-manager/commit/ac830cbe7f3b1ebd7849e586b829d3c077436a0d))
* update pubspec version ([65da6af](https://github.com/revanced/revanced-manager/commit/65da6af3f96550b138dcaf61832a5de23f529b32))
* chore!: merge `dev` to `main` (#830) ([782c012](https://github.com/revanced/revanced-manager/commit/782c0127b1f6f5380bd8ca23c8c2f280e2bfc432)), closes [#830](https://github.com/revanced/revanced-manager/issues/830)
### Features
* add continue anyway button to select from storage dialog ([#810](https://github.com/revanced/revanced-manager/issues/810)) ([affba66](https://github.com/revanced/revanced-manager/commit/affba669ce1ca6866a1dd1bd801e3f33e4bfe051))
* add option to import/export keystore ([#776](https://github.com/revanced/revanced-manager/issues/776)) ([dca2d4f](https://github.com/revanced/revanced-manager/commit/dca2d4fe126a6966a094d335e0f27bb62d76c5e8))
* appreciation message for new contributors ([0a1f2da](https://github.com/revanced/revanced-manager/commit/0a1f2da33da7d44f0613b19f3e6b2b7b50240548))
* auto select default patches ([4c9cb56](https://github.com/revanced/revanced-manager/commit/4c9cb560e3e38295a5140419f2565b478cb6c497))
* change `continue anyways` to `cancel` ([cfc866b](https://github.com/revanced/revanced-manager/commit/cfc866bef2497bc1675bf5dea834cea59d9cc969))
* **ci:** update crowdin workflow to use new main branch ([ded59d2](https://github.com/revanced/revanced-manager/commit/ded59d2da0d193b2dea4a5a7f2fc8eefaceecc0a))
* confirmation dialog for deleting keystore ([#764](https://github.com/revanced/revanced-manager/issues/764)) ([054afbb](https://github.com/revanced/revanced-manager/commit/054afbbedd70a1933d8241ff5b63a772f90b555f))
* disable selecting installed apps for nonroot ([bb681e3](https://github.com/revanced/revanced-manager/commit/bb681e31c9c4e8a5b7b0c883edd1bc5c28505627))
* improve suggested app version text ([#822](https://github.com/revanced/revanced-manager/issues/822)) ([ad17995](https://github.com/revanced/revanced-manager/commit/ad17995f2883682f67eb42b1f82ca865fba86ef9))
* improve ux ([#752](https://github.com/revanced/revanced-manager/issues/752)) ([3b677f8](https://github.com/revanced/revanced-manager/commit/3b677f8ae3739c079e2116417fef6ed395c2ff06)), closes [#782](https://github.com/revanced/revanced-manager/issues/782)
* progress bar for manager updates ([f5aafdb](https://github.com/revanced/revanced-manager/commit/f5aafdb7d6f51386b667abbccf7f2521ef664ba5))
* remove notice about stale development [skip ci] ([62f7a82](https://github.com/revanced/revanced-manager/commit/62f7a820d8ee2506376306e119698d427de745ef))
* resetting source to default dismiss the sources pop-up ([#797](https://github.com/revanced/revanced-manager/issues/797)) ([62ef1c8](https://github.com/revanced/revanced-manager/commit/62ef1c88fe0352d3962f8c73edff4b99ea347c0f))
* trim extra space when setting custom source ([#771](https://github.com/revanced/revanced-manager/issues/771)) ([37b583f](https://github.com/revanced/revanced-manager/commit/37b583f560910c0b5da2a8e64de9b53f5a26bb56))
* warning for armv7 devices ([cdfb09f](https://github.com/revanced/revanced-manager/commit/cdfb09fbfa8e74d84ddcc91565489c3c5b61dfa2))
### BREAKING CHANGES
* this is to release as 1.0.0

View File

@@ -3,15 +3,11 @@
The official ReVanced Manager based on Flutter. The official ReVanced Manager based on Flutter.
## 🔽 Download ## 🔽 Download
To download the Alpha version of Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file. To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
## 📝 Prerequisites ## 📝 Prerequisites
1. Android 8 or higher 1. Android 8 or higher
2. Does not work on some armv7 devices 2. Does not work on some armv7 devices
3. [Vanced MicroG](https://github.com/TeamVanced/VancedMicroG/releases) required for YouTube and YouTube Music (Only for non-root)
## ⚠️ Disclaimer
*Please note that even though we're releasing the Manager, it is an ALPHA version. There's a big chance that the Manager might not work at all for you.*
## 🔴 Issues ## 🔴 Issues
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose). For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
@@ -28,7 +24,7 @@ If you wish to translate ReVanced Manager, we're accepting translations on [Crow
## 🛠️ Building Manager from source ## 🛠️ Building Manager from source
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install) 1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally 2. Clone the repository locally
3. Add your github token in gradle.properties like [this](https://github.com/revanced/revanced-documentation/blob/main/docs/revanced-development/2_building_from_source.md) 3. Add your github token in gradle.properties like [this](https://github.com/revanced/revanced-manager/blob/docs/docs/5_building-from-source.md)
4. Open the project in terminal 4. Open the project in terminal
5. Run `flutter pub get` in terminal 5. Run `flutter pub get` in terminal
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull) 6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)

View File

@@ -11,23 +11,155 @@ include: package:flutter_lints/flutter.yaml
analyzer: analyzer:
exclude: exclude:
- lib/utils/env_class.g.dart - lib/app/app.locator.dart
- lib/app/app.router.dart
- lib/models/patch.g.dart
- lib/models/patched_application.g.dart
linter: linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule - always_declare_return_types
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - require_trailing_commas
- always_put_control_body_on_new_line
# Additional information about this file can be found at - always_require_non_null_named_parameters
# https://dart.dev/guides/language/analysis-options - always_use_package_imports # we do this commonly
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_double_and_int_checks
- avoid_empty_else
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_escaping_inner_quotes
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
- avoid_implementing_value_types
- avoid_init_to_null
- avoid_js_rounded_ints
- avoid_null_checks_in_equality_operators
- avoid_print
- avoid_redundant_argument_values
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_future
- avoid_returning_null_for_void
- avoid_setters_without_getters
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_type_to_string
- avoid_types_as_parameter_names
- avoid_unnecessary_containers
- avoid_void_async
- avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- cast_nullable_to_non_nullable
- close_sinks # not reliable enough
- control_flow_in_finally
- curly_braces_in_flow_control_structures
- depend_on_referenced_packages
- deprecated_consistency
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- eol_at_end_of_file
- exhaustive_cases
- file_names
- flutter_style_todos
- hash_and_equals
- implementation_imports
- iterable_contains_unrelated_type
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- library_private_types_in_public_api
- list_remove_unrelated_type
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- non_constant_identifier_names
- noop_primitive_operations
- null_check_on_nullable_type_parameter
- null_closures
- overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_contains
- prefer_equal_for_default_values
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_for_elements_to_map_fromIterable
- prefer_foreach
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
- prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018
- prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- prefer_void_to_null
- provide_deprecation_message
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- tighten_type_of_initializing_formals
- type_init_formals
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_checks
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- unrelated_type_equality_checks
- unsafe_html
- use_build_context_synchronously
- use_full_hex_values_for_flutter_colors
- use_function_type_syntax_for_parameters
- use_if_null_to_convert_nulls_to_bools
- use_is_even_rather_than_modulo
- use_key_in_widget_constructors
- use_late_for_private_fields_and_variables
- use_named_constants
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_test_throws_matchers
- valid_regexps
- void_checks

View File

@@ -30,6 +30,7 @@ android {
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11
} }
@@ -48,6 +49,7 @@ android {
targetSdkVersion 33 targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
multiDexEnabled true
} }
buildTypes { buildTypes {
@@ -71,7 +73,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced // ReVanced
implementation "app.revanced:revanced-patcher:6.3.2" implementation "app.revanced:revanced-patcher:7.0.0"
// Signing & aligning // Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
@@ -81,4 +83,11 @@ dependencies {
implementation("org.microg:cronet-common:$cronetVersion") implementation("org.microg:cronet-common:$cronetVersion")
implementation("org.microg:cronet-native:$cronetVersion") implementation("org.microg:cronet-native:$cronetVersion")
// Core libraries
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// Window
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java:1.0.0'
} }

View File

@@ -17,7 +17,8 @@
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:largeHeap="true"
android:extractNativeLibs="true"> android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -28,8 +29,7 @@
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"/>
/>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

View File

@@ -43,8 +43,9 @@ class MainActivity : FlutterActivity() {
val integrationsPath = call.argument<String>("integrationsPath") val integrationsPath = call.argument<String>("integrationsPath")
val selectedPatches = call.argument<List<String>>("selectedPatches") val selectedPatches = call.argument<List<String>>("selectedPatches")
val cacheDirPath = call.argument<String>("cacheDirPath") val cacheDirPath = call.argument<String>("cacheDirPath")
val mergeIntegrations = call.argument<Boolean>("mergeIntegrations")
val keyStoreFilePath = call.argument<String>("keyStoreFilePath") val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
val keystorePassword = call.argument<String>("keystorePassword")
if (patchBundleFilePath != null && if (patchBundleFilePath != null &&
originalFilePath != null && originalFilePath != null &&
inputFilePath != null && inputFilePath != null &&
@@ -53,8 +54,8 @@ class MainActivity : FlutterActivity() {
integrationsPath != null && integrationsPath != null &&
selectedPatches != null && selectedPatches != null &&
cacheDirPath != null && cacheDirPath != null &&
mergeIntegrations != null && keyStoreFilePath != null &&
keyStoreFilePath != null keystorePassword != null
) { ) {
runPatcher( runPatcher(
result, result,
@@ -66,8 +67,8 @@ class MainActivity : FlutterActivity() {
integrationsPath, integrationsPath,
selectedPatches, selectedPatches,
cacheDirPath, cacheDirPath,
mergeIntegrations, keyStoreFilePath,
keyStoreFilePath keystorePassword
) )
} else { } else {
result.notImplemented() result.notImplemented()
@@ -88,8 +89,8 @@ class MainActivity : FlutterActivity() {
integrationsPath: String, integrationsPath: String,
selectedPatches: List<String>, selectedPatches: List<String>,
cacheDirPath: String, cacheDirPath: String,
mergeIntegrations: Boolean, keyStoreFilePath: String,
keyStoreFilePath: String keystorePassword: String
) { ) {
val originalFile = File(originalFilePath) val originalFile = File(originalFilePath)
val inputFile = File(inputFilePath) val inputFile = File(inputFilePath)
@@ -139,19 +140,17 @@ class MainActivity : FlutterActivity() {
mapOf("progress" to 0.3, "header" to "", "log" to "") mapOf("progress" to 0.3, "header" to "", "log" to "")
) )
} }
if (mergeIntegrations) { handler.post {
handler.post { installerChannel.invokeMethod(
installerChannel.invokeMethod( "update",
"update", mapOf(
mapOf( "progress" to 0.4,
"progress" to 0.4, "header" to "Merging integrations...",
"header" to "Merging integrations...", "log" to "Merging integrations"
"log" to "Merging integrations"
)
) )
} )
patcher.addFiles(listOf(integrations)) {}
} }
patcher.addIntegrations(listOf(integrations)) {}
handler.post { handler.post {
installerChannel.invokeMethod( installerChannel.invokeMethod(
@@ -248,7 +247,7 @@ class MainActivity : FlutterActivity() {
// Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile) // Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile)
try { try {
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile) Signer("ReVanced", keystorePassword).signApk(patchedFile, outFile, keyStoreFile)
} catch (e: Exception) { } catch (e: Exception) {
//log to console //log to console
print("Error signing apk: ${e.message}") print("Error signing apk: ${e.message}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1B1B1B</color>
</resources>

View File

@@ -1,6 +1,6 @@
buildscript { buildscript {
ext.cronetVersion = '102.5005.125' ext.cronetVersion = '102.5005.125'
ext.kotlin_version = '1.7.20' ext.kotlin_version = '1.7.10'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

View File

@@ -1,6 +1,6 @@
#Mon May 09 12:07:41 MSK 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-1-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -13,6 +13,7 @@
"settingsTab": "Settings" "settingsTab": "Settings"
}, },
"homeView": { "homeView": {
"refreshSuccess": "Refreshed successfully",
"widgetTitle": "Dashboard", "widgetTitle": "Dashboard",
"updatesSubtitle": "Updates", "updatesSubtitle": "Updates",
"patchedSubtitle": "Patched applications", "patchedSubtitle": "Patched applications",
@@ -47,7 +48,8 @@
"patcherView": { "patcherView": {
"widgetTitle": "Patcher", "widgetTitle": "Patcher",
"patchButton": "Patch", "patchButton": "Patch",
"patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?" "patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?",
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported. Proceed anyways?"
}, },
"appSelectorCard": { "appSelectorCard": {
"widgetTitle": "Select an application", "widgetTitle": "Select an application",
@@ -55,7 +57,7 @@
"widgetSubtitle": "No application selected", "widgetSubtitle": "No application selected",
"noAppsLabel": "No applications found", "noAppsLabel": "No applications found",
"currentVersion": "Current", "currentVersion": "Current",
"recommendedVersion": "Recommended", "suggestedVersion": "Suggested",
"anyVersion": "any" "anyVersion": "any"
}, },
"patchSelectorCard": { "patchSelectorCard": {
@@ -71,20 +73,25 @@
"appSelectorView": { "appSelectorView": {
"viewTitle": "Select an application", "viewTitle": "Select an application",
"searchBarHint": "Search applications", "searchBarHint": "Search applications",
"selectFromStorageButton": "Select from storage",
"storageButton": "Storage", "storageButton": "Storage",
"errorMessage": "Unable to use selected application" "errorMessage": "Unable to use selected application",
"downloadToast": "Download function is not available yet",
"featureNotAvailable": "Feature not implemented",
"featureNotAvailableText": "This feature has not been added yet for non-root. You'll need to select APK files from storage for now."
}, },
"patchesSelectorView": { "patchesSelectorView": {
"viewTitle": "Select patches", "viewTitle": "Select patches",
"searchBarHint": "Search patches", "searchBarHint": "Search patches",
"doneButton": "Done", "doneButton": "Done",
"recommended": "Recommended", "default": "Default",
"all": "All", "defaultTooltip": "Select all default patches",
"none": "None", "none": "None",
"noneTooltip": "Deselect all patches",
"loadPatchesSelection": "Load patches selection", "loadPatchesSelection": "Load patches selection",
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.", "noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
"noPatchesFound": "No patches found for the selected app", "noPatchesFound": "No patches found for the selected app",
"selectAllPatchesWarningContent": "You are about to select all patches, that includes unrecommended patches and can cause unwanted behavior." "selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
}, },
"patchItem": { "patchItem": {
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}", "unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
@@ -145,11 +152,10 @@
"exportSectionTitle": "Import & export", "exportSectionTitle": "Import & export",
"aboutLabel": "About", "aboutLabel": "About",
"snackbarMessage": "Copied to clipboard", "snackbarMessage": "Copied to clipboard",
"sentryLabel": "Sentry logging",
"sentryHint": "Send anonymous logs to help us improve ReVanced Manager",
"restartAppForChanges": "Restart the app to apply changes", "restartAppForChanges": "Restart the app to apply changes",
"deleteKeystoreLabel": "Delete keystore", "deleteKeystoreLabel": "Delete keystore",
"deleteKeystoreHint": "Delete the keystore used to sign the app", "deleteKeystoreHint": "Delete the keystore used to sign the app",
"deleteKeystoreDialogText": "Are you sure you want to delete the keystore used to sign patched applications?",
"deletedKeystore": "Keystore deleted", "deletedKeystore": "Keystore deleted",
"deleteTempDirLabel": "Delete temporary files", "deleteTempDirLabel": "Delete temporary files",
"deleteTempDirHint": "Delete unused temporary files", "deleteTempDirHint": "Delete unused temporary files",
@@ -167,7 +173,17 @@
"jsonSelectorErrorMessage": "Unable to use selected JSON file", "jsonSelectorErrorMessage": "Unable to use selected JSON file",
"deleteLogsLabel": "Delete logs", "deleteLogsLabel": "Delete logs",
"deleteLogsHint": "Delete collected manager logs", "deleteLogsHint": "Delete collected manager logs",
"deletedLogs": "Logs deleted" "deletedLogs": "Logs deleted",
"exportKeystoreLabel": "Export keystore",
"exportKeystoreHint": "Export keystore used to sign apps",
"exportedKeystore": "Keystore exported",
"noKeystoreExportFileFound": "No keystore to export",
"importKeystoreLabel": "Import keystore",
"importKeystoreHint": "Import keystore used to sign apps",
"importedKeystore": "Keystore imported",
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file",
"selectKeystorePassword": "Keystore Password",
"selectKeystorePasswordHint": "Select keystore password used to sign the apk"
}, },
"appInfoView": { "appInfoView": {
"widgetTitle": "App info", "widgetTitle": "App info",

View File

@@ -1,4 +1,3 @@
import 'package:revanced_manager/services/crowdin_api.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
@@ -38,7 +37,6 @@ import 'package:stacked_services/stacked_services.dart';
LazySingleton(classType: PatcherAPI), LazySingleton(classType: PatcherAPI),
LazySingleton(classType: RevancedAPI), LazySingleton(classType: RevancedAPI),
LazySingleton(classType: GithubAPI), LazySingleton(classType: GithubAPI),
LazySingleton(classType: CrowdinAPI),
LazySingleton(classType: Toast), LazySingleton(classType: Toast),
], ],
) )

View File

@@ -1,18 +1,17 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/crowdin_api.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart'; import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:revanced_manager/utils/environment.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked_themes/stacked_themes.dart'; import 'package:stacked_themes/stacked_themes.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/data/latest.dart' as tz;
late SharedPreferences prefs; late SharedPreferences prefs;
@@ -21,38 +20,14 @@ Future main() async {
await setupLocator(); await setupLocator();
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await locator<ManagerAPI>().initialize(); await locator<ManagerAPI>().initialize();
String apiUrl = locator<ManagerAPI>().getApiUrl(); final String apiUrl = locator<ManagerAPI>().getApiUrl();
await locator<RevancedAPI>().initialize(apiUrl); await locator<RevancedAPI>().initialize(apiUrl);
await locator<CrowdinAPI>().initialize(); final String repoUrl = locator<ManagerAPI>().getRepoUrl();
bool isSentryEnabled = locator<ManagerAPI>().isSentryEnabled();
String repoUrl = locator<ManagerAPI>().getRepoUrl();
locator<GithubAPI>().initialize(repoUrl); locator<GithubAPI>().initialize(repoUrl);
await locator<PatcherAPI>().initialize(); await locator<PatcherAPI>().initialize();
tz.initializeTimeZones(); tz.initializeTimeZones();
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
await SentryFlutter.init(
(options) {
options
..dsn = isSentryEnabled ? Environment.sentryDSN : ''
..environment = 'alpha'
..release = '0.1'
..tracesSampleRate = 1.0
..anrEnabled = true
..enableOutOfMemoryTracking = true
..sampleRate = isSentryEnabled ? 1.0 : 0.0
..beforeSend = (event, hint) {
if (isSentryEnabled) {
return event;
} else {
return null;
}
} as BeforeSendCallback?;
},
appRunner: () {
runApp(const MyApp());
},
);
runApp(const MyApp()); runApp(const MyApp());
} }
@@ -65,7 +40,7 @@ class MyApp extends StatelessWidget {
// String replaceLocale = rawLocale.replaceAll('_', '-'); // String replaceLocale = rawLocale.replaceAll('_', '-');
// List<String> localeList = replaceLocale.split('-'); // List<String> localeList = replaceLocale.split('-');
// Locale locale = Locale(localeList[0], localeList[1]); // Locale locale = Locale(localeList[0], localeList[1]);
Locale locale = const Locale('en', 'US'); const Locale locale = Locale('en', 'US');
return DynamicThemeBuilder( return DynamicThemeBuilder(
title: 'ReVanced Manager', title: 'ReVanced Manager',
@@ -79,8 +54,9 @@ class MyApp extends StatelessWidget {
useCountryCode: true, useCountryCode: true,
), ),
missingTranslationHandler: (key, locale) { missingTranslationHandler: (key, locale) {
print( log(
'--> Missing translation: key: $key, languageCode: ${locale?.languageCode}'); '--> Missing translation: key: $key, languageCode: ${locale?.languageCode}',
);
}, },
), ),
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,

View File

@@ -5,13 +5,6 @@ part 'patch.g.dart';
@JsonSerializable() @JsonSerializable()
class Patch { class Patch {
final String name;
final String description;
final String version;
final bool excluded;
final List<String> dependencies;
final List<Package> compatiblePackages;
Patch({ Patch({
required this.name, required this.name,
required this.description, required this.description,
@@ -22,6 +15,12 @@ class Patch {
}); });
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json); factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
final String name;
final String description;
final String version;
final bool excluded;
final List<String> dependencies;
final List<Package> compatiblePackages;
Map<String, dynamic> toJson() => _$PatchToJson(this); Map<String, dynamic> toJson() => _$PatchToJson(this);
@@ -37,9 +36,6 @@ class Patch {
@JsonSerializable() @JsonSerializable()
class Package { class Package {
final String name;
final List<String> versions;
Package({ Package({
required this.name, required this.name,
required this.versions, required this.versions,
@@ -47,6 +43,8 @@ class Package {
factory Package.fromJson(Map<String, dynamic> json) => factory Package.fromJson(Map<String, dynamic> json) =>
_$PackageFromJson(json); _$PackageFromJson(json);
final String name;
final List<String> versions;
Map toJson() => _$PackageToJson(this); Map toJson() => _$PackageToJson(this);
} }

View File

@@ -6,23 +6,6 @@ part 'patched_application.g.dart';
@JsonSerializable() @JsonSerializable()
class PatchedApplication { class PatchedApplication {
String name;
String packageName;
String originalPackageName;
String version;
final String apkFilePath;
@JsonKey(
fromJson: decodeBase64,
toJson: encodeBase64,
)
Uint8List icon;
DateTime patchDate;
bool isRooted;
bool isFromStorage;
bool hasUpdates;
List<String> appliedPatches;
List<String> changelog;
PatchedApplication({ PatchedApplication({
required this.name, required this.name,
required this.packageName, required this.packageName,
@@ -40,6 +23,22 @@ class PatchedApplication {
factory PatchedApplication.fromJson(Map<String, dynamic> json) => factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
_$PatchedApplicationFromJson(json); _$PatchedApplicationFromJson(json);
String name;
String packageName;
String originalPackageName;
String version;
final String apkFilePath;
@JsonKey(
fromJson: decodeBase64,
toJson: encodeBase64,
)
Uint8List icon;
DateTime patchDate;
bool isRooted;
bool isFromStorage;
bool hasUpdates;
List<String> appliedPatches;
List<String> changelog;
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this); Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);

View File

@@ -1,61 +0,0 @@
import 'package:dio/dio.dart';
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
import 'package:injectable/injectable.dart' hide Environment;
import 'package:revanced_manager/utils/environment.dart';
import 'package:sentry_dio/sentry_dio.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
@lazySingleton
class CrowdinAPI {
late Dio _dio = Dio();
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final apiKey = Environment.crowdinKEY;
Future<void> initialize() async {
try {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.crowdin.com/api/v2',
));
_dio.interceptors.add(_dioCacheManager.interceptor);
_dio.addSentry(
captureFailedRequests: true,
);
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
}
Future<void> clearAllCache() async {
try {
await _dioCacheManager.clearAll();
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
}
Future<List> getLanguages() async {
try {
var response = await _dio.get(
'/projects',
options: buildCacheOptions(
const Duration(hours: 6),
maxStale: const Duration(days: 1),
options: Options(
headers: {
'Authorization': 'Bearer $apiKey',
},
contentType: 'application/json',
),
),
);
List targetLanguages =
await response.data['data'][0]['data']['targetLanguages'];
return targetLanguages;
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return [];
}
}
}

View File

@@ -1,22 +1,25 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:native_dio_adapter/native_dio_adapter.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_dio/sentry_dio.dart';
@lazySingleton @lazySingleton
class GithubAPI { class GithubAPI {
late Dio _dio = Dio(); late Dio _dio = Dio();
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions( final _cacheOptions = CacheOptions(
const Duration(hours: 6), store: MemCacheStore(),
maxStale: const Duration(days: 1), maxStale: const Duration(days: 1),
priority: CachePriority.high,
); );
final Map<String, String> repoAppPath = { final Map<String, String> repoAppPath = {
'com.google.android.youtube': 'youtube', 'com.google.android.youtube': 'youtube',
'com.google.android.apps.youtube.music': 'music', 'com.google.android.apps.youtube.music': 'music',
@@ -28,38 +31,60 @@ class GithubAPI {
'com.spotify.music': 'spotify', 'com.spotify.music': 'spotify',
}; };
void initialize(String repoUrl) async { Future<void> initialize(String repoUrl) async {
try { try {
_dio = Dio(BaseOptions( if (Platform.isIOS || Platform.isMacOS || Platform.isAndroid) {
baseUrl: repoUrl, final CronetEngine androidCronetEngine = await CronetEngine.build(
)); userAgent: 'ReVanced Manager',
enableBrotli: true,
enableQuic: true,
);
_dio.httpClientAdapter =
NativeAdapter(androidCronetEngine: androidCronetEngine);
_dio.interceptors.add(_dioCacheManager.interceptor); _dio = Dio(
_dio.addSentry( BaseOptions(
captureFailedRequests: true, baseUrl: repoUrl,
),
);
}
_dio = Dio(
BaseOptions(
baseUrl: repoUrl,
),
); );
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s); _dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
} }
} }
Future<void> clearAllCache() async { Future<void> clearAllCache() async {
try { try {
await _dioCacheManager.clearAll(); await _cacheOptions.store!.clean();
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }
Future<Map<String, dynamic>?> getLatestRelease(String repoName) async { Future<Map<String, dynamic>?> getLatestRelease(
String repoName,
) async {
try { try {
var response = await _dio.get( final response = await _dio.get(
'/repos/$repoName/releases', '/repos/$repoName/releases',
options: _cacheOptions,
); );
return response.data[0]; return response.data[0];
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return null; return null;
} }
} }
@@ -69,18 +94,17 @@ class GithubAPI {
String repoName, String repoName,
DateTime since, DateTime since,
) async { ) async {
String path = final String path =
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}'; 'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
try { try {
var response = await _dio.get( final response = await _dio.get(
'/repos/$repoName/commits', '/repos/$repoName/commits',
queryParameters: { queryParameters: {
'path': path, 'path': path,
'since': since.toIso8601String(), 'since': since.toIso8601String(),
}, },
options: _cacheOptions,
); );
List<dynamic> commits = response.data; final List<dynamic> commits = response.data;
return commits return commits
.map( .map(
(commit) => (commit['commit']['message']).split('\n')[0] + (commit) => (commit['commit']['message']).split('\n')[0] +
@@ -89,17 +113,23 @@ class GithubAPI {
'\n' as String, '\n' as String,
) )
.toList(); .toList();
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
return List.empty(); print(e);
}
} }
return [];
} }
Future<File?> getLatestReleaseFile(String extension, String repoName) async { Future<File?> getLatestReleaseFile(
String extension,
String repoName,
) async {
try { try {
Map<String, dynamic>? release = await getLatestRelease(repoName); final Map<String, dynamic>? release =
await getLatestRelease(repoName);
if (release != null) { if (release != null) {
Map<String, dynamic>? asset = final Map<String, dynamic>? asset =
(release['assets'] as List<dynamic>).firstWhereOrNull( (release['assets'] as List<dynamic>).firstWhereOrNull(
(asset) => (asset['name'] as String).endsWith(extension), (asset) => (asset['name'] as String).endsWith(extension),
); );
@@ -109,9 +139,10 @@ class GithubAPI {
); );
} }
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
return null; print(e);
}
} }
return null; return null;
} }
@@ -119,29 +150,35 @@ class GithubAPI {
Future<List<Patch>> getPatches(String repoName) async { Future<List<Patch>> getPatches(String repoName) async {
List<Patch> patches = []; List<Patch> patches = [];
try { try {
File? f = await getLatestReleaseFile('.json', repoName); final File? f = await getLatestReleaseFile('.json', repoName);
if (f != null) { if (f != null) {
List<dynamic> list = jsonDecode(f.readAsStringSync()); final List<dynamic> list = jsonDecode(f.readAsStringSync());
patches = list.map((patch) => Patch.fromJson(patch)).toList(); patches = list.map((patch) => Patch.fromJson(patch)).toList();
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
return List.empty(); print(e);
}
} }
return patches; return patches;
} }
Future<String> getLastestReleaseVersion(String repoName) async { Future<String> getLastestReleaseVersion(String repoName) async {
try { try {
Map<String, dynamic>? release = await getLatestRelease(repoName); final Map<String, dynamic>? release =
await getLatestRelease(repoName);
if (release != null) { if (release != null) {
return release['tag_name']; return release['tag_name'];
} else { } else {
return 'Unknown'; return 'Unknown';
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
return ''; print(e);
}
return 'Unknown';
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -10,7 +11,7 @@ import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@lazySingleton @lazySingleton
@@ -20,8 +21,12 @@ class ManagerAPI {
final RootAPI _rootAPI = RootAPI(); final RootAPI _rootAPI = RootAPI();
final String patcherRepo = 'revanced-patcher'; final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli'; final String cliRepo = 'revanced-cli';
late String storedPatchesFile = '/selected-patches.json';
late SharedPreferences _prefs; late SharedPreferences _prefs;
bool isRooted = false;
String storedPatchesFile = '/selected-patches.json';
String keystoreFile =
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore';
String defaultKeystorePassword = 's3cur3p@ssw0rd';
String defaultApiUrl = 'https://releases.revanced.app/'; String defaultApiUrl = 'https://releases.revanced.app/';
String defaultRepoUrl = 'https://api.github.com'; String defaultRepoUrl = 'https://api.github.com';
String defaultPatcherRepo = 'revanced/revanced-patcher'; String defaultPatcherRepo = 'revanced/revanced-patcher';
@@ -29,9 +34,14 @@ class ManagerAPI {
String defaultIntegrationsRepo = 'revanced/revanced-integrations'; String defaultIntegrationsRepo = 'revanced/revanced-integrations';
String defaultCliRepo = 'revanced/revanced-cli'; String defaultCliRepo = 'revanced/revanced-cli';
String defaultManagerRepo = 'revanced/revanced-manager'; String defaultManagerRepo = 'revanced/revanced-manager';
String? patchesVersion = '';
bool isDefaultPatchesRepo() {
return getPatchesRepo() == 'revanced/revanced-patches';
}
Future<void> initialize() async { Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
isRooted = await _rootAPI.isRooted();
storedPatchesFile = storedPatchesFile =
(await getApplicationDocumentsDirectory()).path + storedPatchesFile; (await getApplicationDocumentsDirectory()).path + storedPatchesFile;
} }
@@ -98,14 +108,6 @@ class ManagerAPI {
await _prefs.setBool('useDarkTheme', value); await _prefs.setBool('useDarkTheme', value);
} }
bool isSentryEnabled() {
return _prefs.getBool('sentryEnabled') ?? true;
}
Future<void> setSentryStatus(bool value) async {
await _prefs.setBool('sentryEnabled', value);
}
bool areUniversalPatchesEnabled() { bool areUniversalPatchesEnabled() {
return _prefs.getBool('universalPatchesEnabled') ?? false; return _prefs.getBool('universalPatchesEnabled') ?? false;
} }
@@ -122,6 +124,14 @@ class ManagerAPI {
await _prefs.setBool('experimentalPatchesEnabled', value); await _prefs.setBool('experimentalPatchesEnabled', value);
} }
Future<void> setKeystorePassword(String password) async {
await _prefs.setString('keystorePassword', password);
}
String getKeystorePassword() {
return _prefs.getString('keystorePassword') ?? defaultKeystorePassword;
}
Future<void> deleteTempFolder() async { Future<void> deleteTempFolder() async {
final Directory dir = Directory('/data/local/tmp/revanced-manager'); final Directory dir = Directory('/data/local/tmp/revanced-manager');
if (await dir.exists()) { if (await dir.exists()) {
@@ -131,29 +141,34 @@ class ManagerAPI {
Future<void> deleteKeystore() async { Future<void> deleteKeystore() async {
final File keystore = File( final File keystore = File(
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore'); keystoreFile,
);
if (await keystore.exists()) { if (await keystore.exists()) {
await keystore.delete(); await keystore.delete();
} }
} }
List<PatchedApplication> getPatchedApps() { List<PatchedApplication> getPatchedApps() {
List<String> apps = _prefs.getStringList('patchedApps') ?? []; final List<String> apps = _prefs.getStringList('patchedApps') ?? [];
return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList(); return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList();
} }
Future<void> setPatchedApps(List<PatchedApplication> patchedApps) async { Future<void> setPatchedApps(
List<PatchedApplication> patchedApps,
) async {
if (patchedApps.length > 1) { if (patchedApps.length > 1) {
patchedApps.sort((a, b) => a.name.compareTo(b.name)); patchedApps.sort((a, b) => a.name.compareTo(b.name));
} }
await _prefs.setStringList('patchedApps', await _prefs.setStringList(
patchedApps.map((a) => json.encode(a.toJson())).toList()); 'patchedApps',
patchedApps.map((a) => json.encode(a.toJson())).toList(),
);
} }
Future<void> savePatchedApp(PatchedApplication app) async { Future<void> savePatchedApp(PatchedApplication app) async {
List<PatchedApplication> patchedApps = getPatchedApps(); final List<PatchedApplication> patchedApps = getPatchedApps();
patchedApps.removeWhere((a) => a.packageName == app.packageName); patchedApps.removeWhere((a) => a.packageName == app.packageName);
ApplicationWithIcon? installed = await DeviceApps.getApp( final ApplicationWithIcon? installed = await DeviceApps.getApp(
app.packageName, app.packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
@@ -167,17 +182,19 @@ class ManagerAPI {
} }
Future<void> deletePatchedApp(PatchedApplication app) async { Future<void> deletePatchedApp(PatchedApplication app) async {
List<PatchedApplication> patchedApps = getPatchedApps(); final List<PatchedApplication> patchedApps = getPatchedApps();
patchedApps.removeWhere((a) => a.packageName == app.packageName); patchedApps.removeWhere((a) => a.packageName == app.packageName);
await setPatchedApps(patchedApps); await setPatchedApps(patchedApps);
} }
void clearAllData() async { Future<void> clearAllData() async {
try { try {
_revancedAPI.clearAllCache(); _revancedAPI.clearAllCache();
_githubAPI.clearAllCache(); _githubAPI.clearAllCache();
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }
@@ -187,21 +204,23 @@ class ManagerAPI {
Future<List<Patch>> getPatches() async { Future<List<Patch>> getPatches() async {
try { try {
String repoName = getPatchesRepo(); final String repoName = getPatchesRepo();
if (repoName == defaultPatchesRepo) { if (repoName == defaultPatchesRepo) {
return await _revancedAPI.getPatches(); return await _revancedAPI.getPatches();
} else { } else {
return await _githubAPI.getPatches(repoName); return await _githubAPI.getPatches(repoName);
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return []; return [];
} }
} }
Future<File?> downloadPatches() async { Future<File?> downloadPatches() async {
try { try {
String repoName = getPatchesRepo(); final String repoName = getPatchesRepo();
if (repoName == defaultPatchesRepo) { if (repoName == defaultPatchesRepo) {
return await _revancedAPI.getLatestReleaseFile( return await _revancedAPI.getLatestReleaseFile(
'.jar', '.jar',
@@ -210,15 +229,17 @@ class ManagerAPI {
} else { } else {
return await _githubAPI.getLatestReleaseFile('.jar', repoName); return await _githubAPI.getLatestReleaseFile('.jar', repoName);
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return null; return null;
} }
} }
Future<File?> downloadIntegrations() async { Future<File?> downloadIntegrations() async {
try { try {
String repoName = getIntegrationsRepo(); final String repoName = getIntegrationsRepo();
if (repoName == defaultIntegrationsRepo) { if (repoName == defaultIntegrationsRepo) {
return await _revancedAPI.getLatestReleaseFile( return await _revancedAPI.getLatestReleaseFile(
'.apk', '.apk',
@@ -227,22 +248,33 @@ class ManagerAPI {
} else { } else {
return await _githubAPI.getLatestReleaseFile('.apk', repoName); return await _githubAPI.getLatestReleaseFile('.apk', repoName);
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return null; return null;
} }
} }
Future<File?> downloadManager() async { Future<File?> downloadManager() async {
return await _revancedAPI.getLatestReleaseFile('.apk', defaultManagerRepo); return await _revancedAPI.getLatestReleaseFile(
'.apk',
defaultManagerRepo,
);
} }
Future<String?> getLatestPatcherReleaseTime() async { Future<String?> getLatestPatcherReleaseTime() async {
return await _revancedAPI.getLatestReleaseTime('.gz', defaultPatcherRepo); return await _revancedAPI.getLatestReleaseTime(
'.gz',
defaultPatcherRepo,
);
} }
Future<String?> getLatestManagerReleaseTime() async { Future<String?> getLatestManagerReleaseTime() async {
return await _revancedAPI.getLatestReleaseTime('.apk', defaultManagerRepo); return await _revancedAPI.getLatestReleaseTime(
'.apk',
defaultManagerRepo,
);
} }
Future<String?> getLatestManagerVersion() async { Future<String?> getLatestManagerVersion() async {
@@ -260,16 +292,29 @@ class ManagerAPI {
} }
Future<String> getCurrentManagerVersion() async { Future<String> getCurrentManagerVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); final PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version; return packageInfo.version;
} }
Future<String?> getCurrentPatchesVersion() async {
if (isDefaultPatchesRepo()) {
patchesVersion = await getLatestPatchesVersion();
// print('Patches version: $patchesVersion');
return patchesVersion ?? '0.0.0';
} else {
// fetch from github
patchesVersion =
await _githubAPI.getLastestReleaseVersion(getPatchesRepo());
}
return null;
}
Future<List<PatchedApplication>> getAppsToRemove( Future<List<PatchedApplication>> getAppsToRemove(
List<PatchedApplication> patchedApps, List<PatchedApplication> patchedApps,
) async { ) async {
List<PatchedApplication> toRemove = []; final List<PatchedApplication> toRemove = [];
for (PatchedApplication app in patchedApps) { for (final PatchedApplication app in patchedApps) {
bool isRemove = await isAppUninstalled(app); final bool isRemove = await isAppUninstalled(app);
if (isRemove) { if (isRemove) {
toRemove.add(app); toRemove.add(app);
} }
@@ -280,13 +325,13 @@ class ManagerAPI {
Future<List<PatchedApplication>> getUnsavedApps( Future<List<PatchedApplication>> getUnsavedApps(
List<PatchedApplication> patchedApps, List<PatchedApplication> patchedApps,
) async { ) async {
List<PatchedApplication> unsavedApps = []; final List<PatchedApplication> unsavedApps = [];
bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { if (hasRootPermissions) {
List<String> installedApps = await _rootAPI.getInstalledApps(); final List<String> installedApps = await _rootAPI.getInstalledApps();
for (String packageName in installedApps) { for (final String packageName in installedApps) {
if (!patchedApps.any((app) => app.packageName == packageName)) { if (!patchedApps.any((app) => app.packageName == packageName)) {
ApplicationWithIcon? application = await DeviceApps.getApp( final ApplicationWithIcon? application = await DeviceApps.getApp(
packageName, packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
@@ -307,15 +352,13 @@ class ManagerAPI {
} }
} }
} }
List<Application> userApps = await DeviceApps.getInstalledApplications( final List<Application> userApps =
includeSystemApps: false, await DeviceApps.getInstalledApplications();
includeAppIcons: false, for (final Application app in userApps) {
);
for (Application app in userApps) {
if (app.packageName.startsWith('app.revanced') && if (app.packageName.startsWith('app.revanced') &&
!app.packageName.startsWith('app.revanced.manager.') && !app.packageName.startsWith('app.revanced.manager.') &&
!patchedApps.any((uapp) => uapp.packageName == app.packageName)) { !patchedApps.any((uapp) => uapp.packageName == app.packageName)) {
ApplicationWithIcon? application = await DeviceApps.getApp( final ApplicationWithIcon? application = await DeviceApps.getApp(
app.packageName, app.packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
@@ -329,7 +372,6 @@ class ManagerAPI {
apkFilePath: application.apkFilePath, apkFilePath: application.apkFilePath,
icon: application.icon, icon: application.icon,
patchDate: DateTime.now(), patchDate: DateTime.now(),
isRooted: false,
), ),
); );
} }
@@ -339,25 +381,29 @@ class ManagerAPI {
} }
Future<void> reAssessSavedApps() async { Future<void> reAssessSavedApps() async {
List<PatchedApplication> patchedApps = getPatchedApps(); final List<PatchedApplication> patchedApps = getPatchedApps();
List<PatchedApplication> unsavedApps = await getUnsavedApps(patchedApps); final List<PatchedApplication> unsavedApps =
await getUnsavedApps(patchedApps);
patchedApps.addAll(unsavedApps); patchedApps.addAll(unsavedApps);
List<PatchedApplication> toRemove = await getAppsToRemove(patchedApps); final List<PatchedApplication> toRemove =
await getAppsToRemove(patchedApps);
patchedApps.removeWhere((a) => toRemove.contains(a)); patchedApps.removeWhere((a) => toRemove.contains(a));
for (PatchedApplication app in patchedApps) { for (final PatchedApplication app in patchedApps) {
app.hasUpdates = app.hasUpdates =
await hasAppUpdates(app.originalPackageName, app.patchDate); await hasAppUpdates(app.originalPackageName, app.patchDate);
app.changelog = app.changelog =
await getAppChangelog(app.originalPackageName, app.patchDate); await getAppChangelog(app.originalPackageName, app.patchDate);
if (!app.hasUpdates) { if (!app.hasUpdates) {
String? currentInstalledVersion = final String? currentInstalledVersion =
(await DeviceApps.getApp(app.packageName))?.versionName; (await DeviceApps.getApp(app.packageName))?.versionName;
if (currentInstalledVersion != null) { if (currentInstalledVersion != null) {
String currentSavedVersion = app.version; final String currentSavedVersion = app.version;
int currentInstalledVersionInt = int.parse( final int currentInstalledVersionInt = int.parse(
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), '')); currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''),
int currentSavedVersionInt = );
int.parse(currentSavedVersion.replaceAll(RegExp('[^0-9]'), '')); final int currentSavedVersionInt = int.parse(
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
);
if (currentInstalledVersionInt > currentSavedVersionInt) { if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true; app.hasUpdates = true;
} }
@@ -369,9 +415,9 @@ class ManagerAPI {
Future<bool> isAppUninstalled(PatchedApplication app) async { Future<bool> isAppUninstalled(PatchedApplication app) async {
bool existsRoot = false; bool existsRoot = false;
bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName); final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
if (app.isRooted) { if (app.isRooted) {
bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { if (hasRootPermissions) {
existsRoot = await _rootAPI.isAppInstalled(app.packageName); existsRoot = await _rootAPI.isAppInstalled(app.packageName);
} }
@@ -380,8 +426,11 @@ class ManagerAPI {
return !existsNonRoot; return !existsNonRoot;
} }
Future<bool> hasAppUpdates(String packageName, DateTime patchDate) async { Future<bool> hasAppUpdates(
List<String> commits = await _githubAPI.getCommits( String packageName,
DateTime patchDate,
) async {
final List<String> commits = await _githubAPI.getCommits(
packageName, packageName,
getPatchesRepo(), getPatchesRepo(),
patchDate, patchDate,
@@ -390,7 +439,9 @@ class ManagerAPI {
} }
Future<List<String>> getAppChangelog( Future<List<String>> getAppChangelog(
String packageName, DateTime patchDate) async { String packageName,
DateTime patchDate,
) async {
List<String> newCommits = await _githubAPI.getCommits( List<String> newCommits = await _githubAPI.getCommits(
packageName, packageName,
getPatchesRepo(), getPatchesRepo(),
@@ -416,38 +467,59 @@ class ManagerAPI {
return app != null && app.isSplit; return app != null && app.isSplit;
} }
Future<void> setSelectedPatches(String app, List<String> patches) async { Future<void> setSelectedPatches(
String app,
List<String> patches,
) async {
final File selectedPatchesFile = File(storedPatchesFile); final File selectedPatchesFile = File(storedPatchesFile);
Map<String, dynamic> patchesMap = await readSelectedPatchesFile(); final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
if (patches.isEmpty) { if (patches.isEmpty) {
patchesMap.remove(app); patchesMap.remove(app);
} else { } else {
patchesMap[app] = patches; patchesMap[app] = patches;
} }
if (selectedPatchesFile.existsSync()) {
selectedPatchesFile.createSync(recursive: true);
}
selectedPatchesFile.writeAsString(jsonEncode(patchesMap)); selectedPatchesFile.writeAsString(jsonEncode(patchesMap));
} }
Future<List<String>> getSelectedPatches(String app) async { // get default patches for app
Map<String, dynamic> patchesMap = await readSelectedPatchesFile(); Future<List<String>> getDefaultPatches() async {
if (patchesMap.isNotEmpty) { final List<Patch> patches = await getPatches();
final List<String> patches = final List<String> defaultPatches = [];
List.from(patchesMap.putIfAbsent(app, () => List.empty())); if (areExperimentalPatchesEnabled() == false) {
return patches; defaultPatches.addAll(
patches
.where(
(element) =>
element.excluded == false && isPatchSupported(element),
)
.map((p) => p.name),
);
} else {
defaultPatches.addAll(
patches
.where((element) => isPatchSupported(element))
.map((p) => p.name),
);
} }
return List.empty(); return defaultPatches;
}
Future<List<String>> getSelectedPatches(String app) async {
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
final List<String> defaultPatches = await getDefaultPatches();
return List.from(patchesMap.putIfAbsent(app, () => defaultPatches));
} }
Future<Map<String, dynamic>> readSelectedPatchesFile() async { Future<Map<String, dynamic>> readSelectedPatchesFile() async {
final File selectedPatchesFile = File(storedPatchesFile); final File selectedPatchesFile = File(storedPatchesFile);
if (selectedPatchesFile.existsSync()) { if (!selectedPatchesFile.existsSync()) {
String string = selectedPatchesFile.readAsStringSync(); return {};
if (string.trim().isEmpty) return {};
return json.decode(string);
} }
return {}; final String string = selectedPatchesFile.readAsStringSync();
if (string.trim().isEmpty) {
return {};
}
return jsonDecode(string);
} }
Future<void> resetLastSelectedPatches() async { Future<void> resetLastSelectedPatches() async {

View File

@@ -1,7 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:app_installer/app_installer.dart'; import 'package:app_installer/app_installer.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -10,9 +13,7 @@ import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:share_extend/share_extend.dart'; import 'package:share_extend/share_extend.dart';
import 'package:cr_file_saver/file_saver.dart';
@lazySingleton @lazySingleton
class PatcherAPI { class PatcherAPI {
@@ -29,7 +30,7 @@ class PatcherAPI {
Future<void> initialize() async { Future<void> initialize() async {
await _loadPatches(); await _loadPatches();
Directory appCache = await getTemporaryDirectory(); final Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache; _dataDir = await getExternalStorageDirectory() ?? appCache;
_tmpDir = Directory('${appCache.path}/patcher'); _tmpDir = Directory('${appCache.path}/patcher');
_keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore'); _keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore');
@@ -47,24 +48,29 @@ class PatcherAPI {
if (_patches.isEmpty) { if (_patches.isEmpty) {
_patches = await _managerAPI.getPatches(); _patches = await _managerAPI.getPatches();
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
_patches = List.empty(); _patches = List.empty();
} }
} }
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(bool showUniversalPatches) async { Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
List<ApplicationWithIcon> filteredApps = []; bool showUniversalPatches,
bool? allAppsIncluded = ) async {
_patches.any((patch) => patch.compatiblePackages.isEmpty) && showUniversalPatches; final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded =
_patches.any((patch) => patch.compatiblePackages.isEmpty) &&
showUniversalPatches;
if (allAppsIncluded) { if (allAppsIncluded) {
var allPackages = await DeviceApps.getInstalledApplications( final allPackages = await DeviceApps.getInstalledApplications(
includeAppIcons: true, includeAppIcons: true,
onlyAppsWithLaunchIntent: true, onlyAppsWithLaunchIntent: true,
); );
allPackages.forEach((pkg) async { for (final pkg in allPackages) {
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) { if (!filteredApps.any((app) => app.packageName == pkg.packageName)) {
var appInfo = await DeviceApps.getApp( final appInfo = await DeviceApps.getApp(
pkg.packageName, pkg.packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
@@ -72,13 +78,13 @@ class PatcherAPI {
filteredApps.add(appInfo); filteredApps.add(appInfo);
} }
} }
}); }
} }
for (Patch patch in _patches) { for (final Patch patch in _patches) {
for (Package package in patch.compatiblePackages) { for (final Package package in patch.compatiblePackages) {
try { try {
if (!filteredApps.any((app) => app.packageName == package.name)) { if (!filteredApps.any((app) => app.packageName == package.name)) {
ApplicationWithIcon? app = await DeviceApps.getApp( final ApplicationWithIcon? app = await DeviceApps.getApp(
package.name, package.name,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
@@ -86,9 +92,10 @@ class PatcherAPI {
filteredApps.add(app); filteredApps.add(app);
} }
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
continue; print(e);
}
} }
} }
} }
@@ -97,44 +104,31 @@ class PatcherAPI {
List<Patch> getFilteredPatches(String packageName) { List<Patch> getFilteredPatches(String packageName) {
if (!filteredPatches.keys.contains(packageName)) { if (!filteredPatches.keys.contains(packageName)) {
List<Patch> patches = _patches final List<Patch> patches = _patches
.where((patch) => .where(
patch.compatiblePackages.isEmpty || (patch) =>
!patch.name.contains('settings') && patch.compatiblePackages.isEmpty ||
patch.compatiblePackages !patch.name.contains('settings') &&
.any((pack) => pack.name == packageName)) patch.compatiblePackages
.any((pack) => pack.name == packageName),
)
.toList(); .toList();
filteredPatches[packageName] = patches; filteredPatches[packageName] = patches;
} }
return filteredPatches[packageName]; return filteredPatches[packageName];
} }
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches) async { Future<List<Patch>> getAppliedPatches(
List<String> appliedPatches,
) async {
return _patches return _patches
.where((patch) => appliedPatches.contains(patch.name)) .where((patch) => appliedPatches.contains(patch.name))
.toList(); .toList();
} }
bool dependencyNeedsIntegrations(String name) { Future<bool> needsResourcePatching(
return name.contains('integrations') || List<Patch> selectedPatches,
_patches.any( ) async {
(patch) =>
patch.name == name &&
(patch.dependencies.any(
(dep) => dependencyNeedsIntegrations(dep),
)),
);
}
Future<bool> needsIntegrations(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dependencyNeedsIntegrations(dep),
),
);
}
Future<bool> needsResourcePatching(List<Patch> selectedPatches) async {
return selectedPatches.any( return selectedPatches.any(
(patch) => patch.dependencies.any( (patch) => patch.dependencies.any(
(dep) => dep.contains('resource-'), (dep) => dep.contains('resource-'),
@@ -155,7 +149,7 @@ class PatcherAPI {
String originalFilePath, String originalFilePath,
) async { ) async {
try { try {
bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { if (hasRootPermissions) {
originalFilePath = await _rootAPI.getOriginalFilePath( originalFilePath = await _rootAPI.getOriginalFilePath(
packageName, packageName,
@@ -163,8 +157,10 @@ class PatcherAPI {
); );
} }
return originalFilePath; return originalFilePath;
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return originalFilePath; return originalFilePath;
} }
} }
@@ -174,11 +170,10 @@ class PatcherAPI {
String originalFilePath, String originalFilePath,
List<Patch> selectedPatches, List<Patch> selectedPatches,
) async { ) async {
bool mergeIntegrations = await needsIntegrations(selectedPatches); final bool includeSettings = await needsSettingsPatch(selectedPatches);
bool includeSettings = await needsSettingsPatch(selectedPatches);
if (includeSettings) { if (includeSettings) {
try { try {
Patch? settingsPatch = _patches.firstWhereOrNull( final Patch? settingsPatch = _patches.firstWhereOrNull(
(patch) => (patch) =>
patch.name.contains('settings') && patch.name.contains('settings') &&
patch.compatiblePackages.any((pack) => pack.name == packageName), patch.compatiblePackages.any((pack) => pack.name == packageName),
@@ -186,24 +181,22 @@ class PatcherAPI {
if (settingsPatch != null) { if (settingsPatch != null) {
selectedPatches.add(settingsPatch); selectedPatches.add(settingsPatch);
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
// ignore print(e);
}
} }
} }
File? patchBundleFile = await _managerAPI.downloadPatches(); final File? patchBundleFile = await _managerAPI.downloadPatches();
File? integrationsFile; final File? integrationsFile = await _managerAPI.downloadIntegrations();
if (mergeIntegrations) {
integrationsFile = await _managerAPI.downloadIntegrations();
}
if (patchBundleFile != null) { if (patchBundleFile != null) {
_dataDir.createSync(); _dataDir.createSync();
_tmpDir.createSync(); _tmpDir.createSync();
Directory workDir = _tmpDir.createTempSync('tmp-'); final Directory workDir = _tmpDir.createTempSync('tmp-');
File inputFile = File('${workDir.path}/base.apk'); final File inputFile = File('${workDir.path}/base.apk');
File patchedFile = File('${workDir.path}/patched.apk'); final File patchedFile = File('${workDir.path}/patched.apk');
_outFile = File('${workDir.path}/out.apk'); _outFile = File('${workDir.path}/out.apk');
Directory cacheDir = Directory('${workDir.path}/cache'); final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync(); cacheDir.createSync();
try { try {
await patcherChannel.invokeMethod( await patcherChannel.invokeMethod(
@@ -217,16 +210,17 @@ class PatcherAPI {
'inputFilePath': inputFile.path, 'inputFilePath': inputFile.path,
'patchedFilePath': patchedFile.path, 'patchedFilePath': patchedFile.path,
'outFilePath': _outFile!.path, 'outFilePath': _outFile!.path,
'integrationsPath': mergeIntegrations ? integrationsFile!.path : '', 'integrationsPath': integrationsFile!.path,
'selectedPatches': selectedPatches.map((p) => p.name).toList(), 'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'cacheDirPath': cacheDir.path, 'cacheDirPath': cacheDir.path,
'mergeIntegrations': mergeIntegrations,
'keyStoreFilePath': _keyStoreFile.path, 'keyStoreFilePath': _keyStoreFile.path,
'keystorePassword': _managerAPI.getKeystorePassword(),
}, },
); );
} on Exception catch (e, s) { } on Exception catch (e) {
print(e); if (kDebugMode) {
throw await Sentry.captureException(e, stackTrace: s); print(e);
}
} }
} }
} }
@@ -235,7 +229,7 @@ class PatcherAPI {
if (_outFile != null) { if (_outFile != null) {
try { try {
if (patchedApp.isRooted) { if (patchedApp.isRooted) {
bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { if (hasRootPermissions) {
return _rootAPI.installApp( return _rootAPI.installApp(
patchedApp.packageName, patchedApp.packageName,
@@ -245,10 +239,14 @@ class PatcherAPI {
} }
} else { } else {
await AppInstaller.installApk(_outFile!.path); await AppInstaller.installApk(_outFile!.path);
return await DeviceApps.isAppInstalled(patchedApp.packageName); return await DeviceApps.isAppInstalled(
patchedApp.packageName,
);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} }
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return false; return false;
} }
} }
@@ -258,67 +256,68 @@ class PatcherAPI {
void exportPatchedFile(String appName, String version) { void exportPatchedFile(String appName, String version) {
try { try {
if (_outFile != null) { if (_outFile != null) {
String newName = _getFileName(appName, version); final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog(
// This is temporary workaround to populate initial file name SaveFileDialogParams(
// ref: https://github.com/Cleveroad/cr_file_saver/issues/7 sourceFilePath: _outFile!.path,
int lastSeparator = _outFile!.path.lastIndexOf('/'); destinationFileName: newName,
String newSourcePath = ),
_outFile!.path.substring(0, lastSeparator + 1) + newName; );
_outFile!.copySync(newSourcePath); }
} on Exception catch (e) {
CRFileSaver.saveFileWithDialog(SaveFileDialogParams( if (kDebugMode) {
sourceFilePath: newSourcePath, destinationFileName: newName)); print(e);
} }
} on Exception catch (e, s) {
Sentry.captureException(e, stackTrace: s);
} }
} }
void sharePatchedFile(String appName, String version) { void sharePatchedFile(String appName, String version) {
try { try {
if (_outFile != null) { if (_outFile != null) {
String newName = _getFileName(appName, version); final String newName = _getFileName(appName, version);
int lastSeparator = _outFile!.path.lastIndexOf('/'); final int lastSeparator = _outFile!.path.lastIndexOf('/');
String newPath = final String newPath =
_outFile!.path.substring(0, lastSeparator + 1) + newName; _outFile!.path.substring(0, lastSeparator + 1) + newName;
File shareFile = _outFile!.copySync(newPath); final File shareFile = _outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file'); ShareExtend.share(shareFile.path, 'file');
} }
} on Exception catch (e, s) { } on Exception catch (e) {
Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }
String _getFileName(String appName, String version) { String _getFileName(String appName, String version) {
String prefix = appName.toLowerCase().replaceAll(' ', '-'); final String prefix = appName.toLowerCase().replaceAll(' ', '-');
String newName = '$prefix-revanced_v$version.apk'; final String newName = '$prefix-revanced_v$version.apk';
return newName; return newName;
} }
Future<void> sharePatcherLog(String logs) async { Future<void> sharePatcherLog(String logs) async {
Directory appCache = await getTemporaryDirectory(); final Directory appCache = await getTemporaryDirectory();
Directory logDir = Directory('${appCache.path}/logs'); final Directory logDir = Directory('${appCache.path}/logs');
logDir.createSync(); logDir.createSync();
String dateTime = DateTime.now() final String dateTime = DateTime.now()
.toIso8601String() .toIso8601String()
.replaceAll('-', '') .replaceAll('-', '')
.replaceAll(':', '') .replaceAll(':', '')
.replaceAll('T', '') .replaceAll('T', '')
.replaceAll('.', ''); .replaceAll('.', '');
File log = File('${logDir.path}/revanced-manager_patcher_$dateTime.log'); final File log =
File('${logDir.path}/revanced-manager_patcher_$dateTime.log');
log.writeAsStringSync(logs); log.writeAsStringSync(logs);
ShareExtend.share(log.path, 'file'); ShareExtend.share(log.path, 'file');
} }
String getRecommendedVersion(String packageName) { String getSuggestedVersion(String packageName) {
Map<String, int> versions = {}; final Map<String, int> versions = {};
for (Patch patch in _patches) { for (final Patch patch in _patches) {
Package? package = patch.compatiblePackages.firstWhereOrNull( final Package? package = patch.compatiblePackages.firstWhereOrNull(
(pack) => pack.name == packageName, (pack) => pack.name == packageName,
); );
if (package != null) { if (package != null) {
for (String version in package.versions) { for (final String version in package.versions) {
versions.update( versions.update(
version, version,
(value) => versions[version]! + 1, (value) => versions[version]! + 1,
@@ -328,7 +327,7 @@ class PatcherAPI {
} }
} }
if (versions.isNotEmpty) { if (versions.isNotEmpty) {
var entries = versions.entries.toList() final entries = versions.entries.toList()
..sort((a, b) => a.value.compareTo(b.value)); ..sort((a, b) => a.value.compareTo(b.value));
versions versions
..clear() ..clear()

View File

@@ -1,69 +1,89 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:native_dio_client/native_dio_client.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:native_dio_adapter/native_dio_adapter.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/utils/check_for_gms.dart'; import 'package:revanced_manager/utils/check_for_gms.dart';
import 'package:timeago/timeago.dart'; import 'package:timeago/timeago.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_dio/sentry_dio.dart';
@lazySingleton @lazySingleton
class RevancedAPI { class RevancedAPI {
late Dio _dio = Dio(); late Dio _dio = Dio();
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions( final _cacheOptions = CacheOptions(
const Duration(hours: 6), store: MemCacheStore(),
maxStale: const Duration(days: 1), maxStale: const Duration(days: 1),
priority: CachePriority.high,
); );
Future<void> initialize(String apiUrl) async { Future<void> initialize(String apiUrl) async {
try { try {
bool isGMSInstalled = await checkForGMS(); final bool isGMSInstalled = await checkForGMS();
if (!isGMSInstalled) { if (!isGMSInstalled) {
_dio = Dio(BaseOptions( _dio = Dio(
baseUrl: apiUrl, BaseOptions(
)); baseUrl: apiUrl,
print('ReVanced API: Using default engine + $isGMSInstalled'); ),
);
log('ReVanced API: Using default engine + $isGMSInstalled');
} else { } else {
_dio = Dio(BaseOptions( if (Platform.isIOS || Platform.isMacOS || Platform.isAndroid) {
baseUrl: apiUrl, final CronetEngine androidCronetEngine = await CronetEngine.build(
)) userAgent: 'ReVanced Manager',
..httpClientAdapter = NativeAdapter(); enableBrotli: true,
print('ReVanced API: Using CronetEngine + $isGMSInstalled'); enableQuic: true,
);
_dio.httpClientAdapter =
NativeAdapter(androidCronetEngine: androidCronetEngine);
_dio = Dio(
BaseOptions(
baseUrl: apiUrl,
),
);
}
log('ReVanced API: Using CronetEngine + $isGMSInstalled');
}
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} }
_dio.interceptors.add(_dioCacheManager.interceptor);
_dio.addSentry(
captureFailedRequests: true,
);
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
} }
} }
Future<void> clearAllCache() async { Future<void> clearAllCache() async {
try { try {
await _dioCacheManager.clearAll(); await _cacheOptions.store!.clean();
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }
Future<Map<String, List<dynamic>>> getContributors() async { Future<Map<String, List<dynamic>>> getContributors() async {
Map<String, List<dynamic>> contributors = {}; final Map<String, List<dynamic>> contributors = {};
try { try {
var response = await _dio.get('/contributors', options: _cacheOptions); final response = await _dio.get('/contributors');
List<dynamic> repositories = response.data['repositories']; final List<dynamic> repositories = response.data['repositories'];
for (Map<String, dynamic> repo in repositories) { for (final Map<String, dynamic> repo in repositories) {
String name = repo['name']; final String name = repo['name'];
contributors[name] = repo['contributors']; contributors[name] = repo['contributors'];
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return {}; return {};
} }
return contributors; return contributors;
@@ -71,11 +91,13 @@ class RevancedAPI {
Future<List<Patch>> getPatches() async { Future<List<Patch>> getPatches() async {
try { try {
var response = await _dio.get('/patches', options: _cacheOptions); final response = await _dio.get('/patches');
List<dynamic> patches = response.data; final List<dynamic> patches = response.data;
return patches.map((patch) => Patch.fromJson(patch)).toList(); return patches.map((patch) => Patch.fromJson(patch)).toList();
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return List.empty(); return List.empty();
} }
} }
@@ -85,15 +107,17 @@ class RevancedAPI {
String repoName, String repoName,
) async { ) async {
try { try {
var response = await _dio.get('/tools', options: _cacheOptions); final response = await _dio.get('/tools');
List<dynamic> tools = response.data['tools']; final List<dynamic> tools = response.data['tools'];
return tools.firstWhereOrNull( return tools.firstWhereOrNull(
(t) => (t) =>
t['repository'] == repoName && t['repository'] == repoName &&
(t['name'] as String).endsWith(extension), (t['name'] as String).endsWith(extension),
); );
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return null; return null;
} }
} }
@@ -103,52 +127,99 @@ class RevancedAPI {
String repoName, String repoName,
) async { ) async {
try { try {
Map<String, dynamic>? release = await _getLatestRelease( final Map<String, dynamic>? release = await _getLatestRelease(
extension, extension,
repoName, repoName,
); );
if (release != null) { if (release != null) {
return release['version']; return release['version'];
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return null; return null;
} }
return null; return null;
} }
Future<File?> getLatestReleaseFile(String extension, String repoName) async { Future<File?> getLatestReleaseFile(
String extension,
String repoName,
) async {
try { try {
Map<String, dynamic>? release = await _getLatestRelease( final Map<String, dynamic>? release = await _getLatestRelease(
extension, extension,
repoName, repoName,
); );
if (release != null) { if (release != null) {
String url = release['browser_download_url']; final String url = release['browser_download_url'];
return await DefaultCacheManager().getSingleFile(url); return await DefaultCacheManager().getSingleFile(url);
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return null; return null;
} }
return null; return null;
} }
StreamController<double> managerUpdateProgress = StreamController<double>();
void updateManagerDownloadProgress(int progress) {
managerUpdateProgress.add(progress.toDouble());
}
Stream<double> getManagerUpdateProgress() {
return managerUpdateProgress.stream;
}
void disposeManagerUpdateProgress() {
managerUpdateProgress.close();
}
Future<File?> downloadManager() async {
final Map<String, dynamic>? release = await _getLatestRelease(
'.apk',
'revanced/revanced-manager',
);
File? outputFile;
await for (final result in DefaultCacheManager().getFileStream(
release!['browser_download_url'] as String,
withProgress: true,
)) {
if (result is DownloadProgress) {
final totalSize = result.totalSize ?? 10000000;
final progress = (result.downloaded / totalSize * 100).round();
updateManagerDownloadProgress(progress);
} else if (result is FileInfo) {
// The download is complete; convert the FileInfo object to a File object
outputFile = File(result.file.path);
}
}
return outputFile;
}
Future<String?> getLatestReleaseTime( Future<String?> getLatestReleaseTime(
String extension, String extension,
String repoName, String repoName,
) async { ) async {
try { try {
Map<String, dynamic>? release = await _getLatestRelease( final Map<String, dynamic>? release = await _getLatestRelease(
extension, extension,
repoName, repoName,
); );
if (release != null) { if (release != null) {
DateTime timestamp = DateTime.parse(release['timestamp'] as String); final DateTime timestamp =
DateTime.parse(release['timestamp'] as String);
return format(timestamp, locale: 'en_short'); return format(timestamp, locale: 'en_short');
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return null; return null;
} }
return null; return null;

View File

@@ -1,5 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:root/root.dart'; import 'package:root/root.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class RootAPI { class RootAPI {
final String _managerDirPath = '/data/local/tmp/revanced-manager'; final String _managerDirPath = '/data/local/tmp/revanced-manager';
@@ -8,10 +8,12 @@ class RootAPI {
Future<bool> isRooted() async { Future<bool> isRooted() async {
try { try {
bool? isRooted = await Root.isRootAvailable(); final bool? isRooted = await Root.isRootAvailable();
return isRooted != null && isRooted; return isRooted != null && isRooted;
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return false; return false;
} }
} }
@@ -24,8 +26,10 @@ class RootAPI {
return isRooted != null && isRooted; return isRooted != null && isRooted;
} }
return false; return false;
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return false; return false;
} }
} }
@@ -70,16 +74,18 @@ class RootAPI {
Future<List<String>> getInstalledApps() async { Future<List<String>> getInstalledApps() async {
try { try {
String? res = await Root.exec( final String? res = await Root.exec(
cmd: 'ls "$_managerDirPath"', cmd: 'ls "$_managerDirPath"',
); );
if (res != null) { if (res != null) {
List<String> apps = res.split('\n'); final List<String> apps = res.split('\n');
apps.removeWhere((pack) => pack.isEmpty); apps.removeWhere((pack) => pack.isEmpty);
return apps.map((pack) => pack.trim()).toList(); return apps.map((pack) => pack.trim()).toList();
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return List.empty(); return List.empty();
} }
return List.empty(); return List.empty();
@@ -125,19 +131,21 @@ class RootAPI {
await installApk(packageName, patchedFilePath); await installApk(packageName, patchedFilePath);
await mountApk(packageName, originalFilePath); await mountApk(packageName, originalFilePath);
return true; return true;
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return false; return false;
} }
} }
Future<void> installServiceDScript(String packageName) async { Future<void> installServiceDScript(String packageName) async {
String content = '#!/system/bin/sh\n' final String content = '#!/system/bin/sh\n'
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n' 'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n'
'base_path=$_managerDirPath/$packageName/base.apk\n' 'base_path=$_managerDirPath/$packageName/base.apk\n'
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n' 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
'[ ! -z \$stock_path ] && mount -o bind \$base_path \$stock_path'; r'[ ! -z $stock_path ] && mount -o bind $base_path $stock_path';
String scriptFilePath = '$_serviceDDirPath/$packageName.sh'; final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
await Root.exec( await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"', cmd: 'echo \'$content\' > "$scriptFilePath"',
); );
@@ -145,10 +153,10 @@ class RootAPI {
} }
Future<void> installPostFsDataScript(String packageName) async { Future<void> installPostFsDataScript(String packageName) async {
String content = '#!/system/bin/sh\n' final String content = '#!/system/bin/sh\n'
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n' 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
'[ ! -z \$stock_path ] && umount -l \$stock_path'; r'[ ! -z $stock_path ] && umount -l $stock_path';
String scriptFilePath = '$_postFsDataDirPath/$packageName.sh'; final String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
await Root.exec( await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"', cmd: 'echo \'$content\' > "$scriptFilePath"',
); );
@@ -156,7 +164,7 @@ class RootAPI {
} }
Future<void> installApk(String packageName, String patchedFilePath) async { Future<void> installApk(String packageName, String patchedFilePath) async {
String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk'; final String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
await Root.exec( await Root.exec(
cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"', cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"',
); );
@@ -169,7 +177,7 @@ class RootAPI {
} }
Future<void> mountApk(String packageName, String originalFilePath) async { Future<void> mountApk(String packageName, String originalFilePath) async {
String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk'; final String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
await Root.exec( await Root.exec(
cmd: 'am force-stop "$packageName"', cmd: 'am force-stop "$packageName"',
); );
@@ -182,7 +190,7 @@ class RootAPI {
} }
Future<bool> isMounted(String packageName) async { Future<bool> isMounted(String packageName) async {
String? res = await Root.exec( final String? res = await Root.exec(
cmd: 'cat /proc/mounts | grep $packageName', cmd: 'cat /proc/mounts | grep $packageName',
); );
return res != null && res.isNotEmpty; return res != null && res.isNotEmpty;
@@ -192,7 +200,7 @@ class RootAPI {
String packageName, String packageName,
String originalFilePath, String originalFilePath,
) async { ) async {
bool isInstalled = await isAppInstalled(packageName); final bool isInstalled = await isAppInstalled(packageName);
if (isInstalled && await isMounted(packageName)) { if (isInstalled && await isMounted(packageName)) {
originalFilePath = '$_managerDirPath/$packageName/original.apk'; originalFilePath = '$_managerDirPath/$packageName/original.apk';
await setPermissions( await setPermissions(
@@ -209,7 +217,8 @@ class RootAPI {
String packageName, String packageName,
String originalFilePath, String originalFilePath,
) async { ) async {
String originalRootPath = '$_managerDirPath/$packageName/original.apk'; final String originalRootPath =
'$_managerDirPath/$packageName/original.apk';
await Root.exec( await Root.exec(
cmd: 'mkdir -p "$_managerDirPath/$packageName"', cmd: 'mkdir -p "$_managerDirPath/$packageName"',
); );

View File

@@ -3,7 +3,6 @@ import 'package:google_fonts/google_fonts.dart';
var lightCustomColorScheme = ColorScheme.fromSeed( var lightCustomColorScheme = ColorScheme.fromSeed(
seedColor: Colors.blue, seedColor: Colors.blue,
brightness: Brightness.light,
primary: const Color(0xff1B73E8), primary: const Color(0xff1B73E8),
); );
@@ -13,7 +12,7 @@ var lightCustomTheme = ThemeData(
navigationBarTheme: NavigationBarThemeData( navigationBarTheme: NavigationBarThemeData(
labelTextStyle: MaterialStateProperty.all( labelTextStyle: MaterialStateProperty.all(
TextStyle( TextStyle(
color: lightCustomColorScheme.secondary, color: lightCustomColorScheme.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -34,13 +33,12 @@ var darkCustomTheme = ThemeData(
navigationBarTheme: NavigationBarThemeData( navigationBarTheme: NavigationBarThemeData(
labelTextStyle: MaterialStateProperty.all( labelTextStyle: MaterialStateProperty.all(
TextStyle( TextStyle(
color: darkCustomColorScheme.secondary, color: darkCustomColorScheme.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
), ),
canvasColor: const Color(0xff1B1A1D), canvasColor: const Color(0xff1B1A1D),
scaffoldBackgroundColor: const Color(0xff1B1A1D), scaffoldBackgroundColor: const Color(0xff1B1A1D),
toggleableActiveColor: const Color(0xffA5CAFF),
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme), textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
); );

View File

@@ -7,65 +7,44 @@ import 'package:revanced_manager/theme.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
class DynamicThemeBuilder extends StatelessWidget { class DynamicThemeBuilder extends StatelessWidget {
final String title;
final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates;
const DynamicThemeBuilder({ const DynamicThemeBuilder({
Key? key, Key? key,
required this.title, required this.title,
required this.home, required this.home,
required this.localizationsDelegates, required this.localizationsDelegates,
}) : super(key: key); }) : super(key: key);
final String title;
final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) { builder: (lightColorScheme, darkColorScheme) {
ThemeData lightDynamicTheme = ThemeData( final ThemeData lightDynamicTheme = ThemeData(
useMaterial3: true, useMaterial3: true,
canvasColor: lightColorScheme?.background,
navigationBarTheme: NavigationBarThemeData( navigationBarTheme: NavigationBarThemeData(
backgroundColor: lightColorScheme?.background,
indicatorColor: lightColorScheme?.primary.withAlpha(150),
labelTextStyle: MaterialStateProperty.all( labelTextStyle: MaterialStateProperty.all(
GoogleFonts.roboto( GoogleFonts.roboto(
color: lightColorScheme?.secondary, color: lightColorScheme?.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
iconTheme: MaterialStateProperty.all(
IconThemeData(
color: lightColorScheme?.secondary,
),
),
), ),
scaffoldBackgroundColor: lightColorScheme?.background,
colorScheme: lightColorScheme?.harmonized(), colorScheme: lightColorScheme?.harmonized(),
toggleableActiveColor: lightColorScheme?.primary,
textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme), textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme),
); );
ThemeData darkDynamicTheme = ThemeData( final ThemeData darkDynamicTheme = ThemeData(
useMaterial3: true, useMaterial3: true,
canvasColor: darkColorScheme?.background,
navigationBarTheme: NavigationBarThemeData( navigationBarTheme: NavigationBarThemeData(
backgroundColor: darkColorScheme?.background,
indicatorColor: darkColorScheme?.primary.withOpacity(0.4),
labelTextStyle: MaterialStateProperty.all( labelTextStyle: MaterialStateProperty.all(
GoogleFonts.roboto( GoogleFonts.roboto(
color: darkColorScheme?.secondary, color: darkColorScheme?.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
iconTheme: MaterialStateProperty.all(
IconThemeData(
color: darkColorScheme?.secondary,
),
),
), ),
scaffoldBackgroundColor: darkColorScheme?.background,
colorScheme: darkColorScheme?.harmonized(), colorScheme: darkColorScheme?.harmonized(),
toggleableActiveColor: darkColorScheme?.primary,
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme), textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
); );
return DynamicTheme( return DynamicTheme(

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
import 'package:stacked/stacked.dart' hide SkeletonLoader;
import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart'; import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/not_installed_app_item.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:stacked/stacked.dart' hide SkeletonLoader;
class AppSelectorView extends StatefulWidget { class AppSelectorView extends StatefulWidget {
const AppSelectorView({Key? key}) : super(key: key); const AppSelectorView({Key? key}) : super(key: key);
@@ -19,7 +20,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<AppSelectorViewModel>.reactive( return ViewModelBuilder<AppSelectorViewModel>.reactive(
onModelReady: (model) => model.initialize(), onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => AppSelectorViewModel(), viewModelBuilder: () => AppSelectorViewModel(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
@@ -36,20 +37,19 @@ class _AppSelectorViewState extends State<AppSelectorView> {
SliverAppBar( SliverAppBar(
pinned: true, pinned: true,
floating: true, floating: true,
snap: false,
title: I18nText( title: I18nText(
'appSelectorView.viewTitle', 'appSelectorView.viewTitle',
child: Text( child: Text(
'', '',
style: TextStyle( style: TextStyle(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
), ),
leading: IconButton( leading: IconButton(
icon: Icon( icon: Icon(
Icons.arrow_back, Icons.arrow_back,
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
@@ -61,7 +61,6 @@ class _AppSelectorViewState extends State<AppSelectorView> {
horizontal: 12.0, horizontal: 12.0,
), ),
child: SearchBar( child: SearchBar(
showSelectIcon: false,
hintText: FlutterI18n.translate( hintText: FlutterI18n.translate(
context, context,
'appSelectorView.searchBarHint', 'appSelectorView.searchBarHint',
@@ -78,7 +77,16 @@ class _AppSelectorViewState extends State<AppSelectorView> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: model.noApps child: model.noApps
? Center( ? Center(
child: I18nText('appSelectorCard.noAppsLabel'), child: I18nText(
'appSelectorView.noApps',
child: Text(
'',
style: TextStyle(
color:
Theme.of(context).textTheme.titleLarge!.color,
),
),
),
) )
: model.apps.isEmpty : model.apps.isEmpty
? const AppSkeletonLoader() ? const AppSkeletonLoader()
@@ -86,20 +94,48 @@ class _AppSelectorViewState extends State<AppSelectorView> {
padding: const EdgeInsets.symmetric(horizontal: 12.0) padding: const EdgeInsets.symmetric(horizontal: 12.0)
.copyWith(bottom: 80), .copyWith(bottom: 80),
child: Column( child: Column(
children: model children: [
.getFilteredApps(_query) ...model
.map((app) => InstalledAppItem( .getFilteredApps(_query)
.map(
(app) => InstalledAppItem(
name: app.appName, name: app.appName,
pkgName: app.packageName, pkgName: app.packageName,
icon: app.icon, icon: app.icon,
patchesCount: patchesCount:
model.patchesCount(app.packageName), model.patchesCount(app.packageName),
suggestedVersion:
model.getSuggestedVersion(
app.packageName,
),
onTap: () { onTap: () {
model.selectApp(app); model.isRooted
Navigator.of(context).pop(); ? model.selectApp(app).then(
(_) => Navigator.of(context)
.pop(),
)
: model.showSelectFromStorageDialog(
context,
);
}, },
)) ),
.toList(), )
.toList(),
...model
.getFilteredAppsNames(_query)
.map(
(app) => NotInstalledAppItem(
name: app,
patchesCount: model.patchesCount(app),
suggestedVersion:
model.getSuggestedVersion(app),
onTap: () {
model.showDownloadToast();
},
),
)
.toList(),
],
), ),
), ),
), ),

View File

@@ -1,37 +1,71 @@
import 'dart:io'; import 'dart:io';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import '../../../services/manager_api.dart';
class AppSelectorViewModel extends BaseViewModel { class AppSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>(); final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
final List<ApplicationWithIcon> apps = []; final List<ApplicationWithIcon> apps = [];
List<String> allApps = [];
bool noApps = false; bool noApps = false;
bool isRooted = false;
int patchesCount(String packageName) { int patchesCount(String packageName) {
return _patcherAPI.getFilteredPatches(packageName).length; return _patcherAPI.getFilteredPatches(packageName).length;
} }
List<Patch> patches = [];
Future<void> initialize() async { Future<void> initialize() async {
apps.addAll(await _patcherAPI.getFilteredInstalledApps(_managerAPI.areUniversalPatchesEnabled())); patches = await _revancedAPI.getPatches();
apps.sort(((a, b) => _patcherAPI isRooted = _managerAPI.isRooted;
.getFilteredPatches(b.packageName)
.length apps.addAll(
.compareTo(_patcherAPI.getFilteredPatches(a.packageName).length))); await _patcherAPI
.getFilteredInstalledApps(_managerAPI.areUniversalPatchesEnabled()),
);
apps.sort(
(a, b) => _patcherAPI
.getFilteredPatches(b.packageName)
.length
.compareTo(_patcherAPI.getFilteredPatches(a.packageName).length),
);
noApps = apps.isEmpty; noApps = apps.isEmpty;
getAllApps();
notifyListeners(); notifyListeners();
} }
void selectApp(ApplicationWithIcon application) async { List<String> getAllApps() {
allApps = patches
.expand((e) => e.compatiblePackages.map((p) => p.name))
.toSet()
.where((name) => !apps.any((app) => app.packageName == name))
.toList();
return allApps;
}
String getSuggestedVersion(String packageName) {
return _patcherAPI.getSuggestedVersion(packageName);
}
Future<void> selectApp(ApplicationWithIcon application) async {
locator<PatcherViewModel>().selectedApp = PatchedApplication( locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName, name: application.appName,
packageName: application.packageName, packageName: application.packageName,
@@ -42,27 +76,102 @@ class AppSelectorViewModel extends BaseViewModel {
patchDate: DateTime.now(), patchDate: DateTime.now(),
); );
locator<PatcherViewModel>().loadLastSelectedPatches(); locator<PatcherViewModel>().loadLastSelectedPatches();
locator<PatcherViewModel>().notifyListeners(); }
Future showSelectFromStorageDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => SimpleDialog(
alignment: Alignment.center,
contentPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
children: [
const SizedBox(height: 10),
Icon(
Icons.block,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 20),
I18nText(
'appSelectorView.featureNotAvailable',
child: const Text(
'',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
wordSpacing: 1.5,
),
),
),
const SizedBox(height: 20),
I18nText(
'appSelectorView.featureNotAvailableText',
child: const Text(
'',
style: TextStyle(
fontSize: 14,
),
),
),
const SizedBox(height: 30),
CustomMaterialButton(
onPressed: () => selectAppFromStorage(context).then(
(_) {
Navigator.pop(context);
Navigator.pop(context);
},
),
label: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.sd_card),
const SizedBox(width: 10),
I18nText('appSelectorView.selectFromStorageButton'),
],
),
),
const SizedBox(height: 10),
CustomMaterialButton(
isFilled: false,
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
label: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(width: 10),
I18nText('cancelButton'),
],
),
),
],
),
);
} }
Future<void> selectAppFromStorage(BuildContext context) async { Future<void> selectAppFromStorage(BuildContext context) async {
try { try {
FilePickerResult? result = await FilePicker.platform.pickFiles( final FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowedExtensions: ['apk'], allowedExtensions: ['apk'],
); );
if (result != null && result.files.single.path != null) { if (result != null && result.files.single.path != null) {
File apkFile = File(result.files.single.path!); final File apkFile = File(result.files.single.path!);
List<String> pathSplit = result.files.single.path!.split("/"); final List<String> pathSplit = result.files.single.path!.split('/');
pathSplit.removeLast(); pathSplit.removeLast();
Directory filePickerCacheDir = Directory(pathSplit.join("/")); final Directory filePickerCacheDir = Directory(pathSplit.join('/'));
Iterable<File> deletableFiles = final Iterable<File> deletableFiles =
(await filePickerCacheDir.list().toList()).whereType<File>(); (await filePickerCacheDir.list().toList()).whereType<File>();
for (var file in deletableFiles) { for (final file in deletableFiles) {
if (file.path != apkFile.path && file.path.endsWith(".apk")) if (file.path != apkFile.path && file.path.endsWith('.apk')) {
file.delete(); file.delete();
}
} }
ApplicationWithIcon? application = await DeviceApps.getAppFromStorage( final ApplicationWithIcon? application =
await DeviceApps.getAppFromStorage(
apkFile.path, apkFile.path,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
@@ -78,21 +187,38 @@ class AppSelectorViewModel extends BaseViewModel {
isFromStorage: true, isFromStorage: true,
); );
locator<PatcherViewModel>().loadLastSelectedPatches(); locator<PatcherViewModel>().loadLastSelectedPatches();
locator<PatcherViewModel>().notifyListeners();
} }
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
_toast.showBottom('appSelectorView.errorMessage'); _toast.showBottom('appSelectorView.errorMessage');
} }
} }
List<ApplicationWithIcon> getFilteredApps(String query) { List<ApplicationWithIcon> getFilteredApps(String query) {
return apps return apps
.where((app) => .where(
query.isEmpty || (app) =>
query.length < 2 || query.isEmpty ||
app.appName.toLowerCase().contains(query.toLowerCase())) query.length < 2 ||
app.appName.toLowerCase().contains(query.toLowerCase()),
)
.toList(); .toList();
} }
List<String> getFilteredAppsNames(String query) {
return allApps
.where(
(app) =>
query.isEmpty ||
query.length < 2 ||
app.toLowerCase().contains(query.toLowerCase()),
)
.toList();
}
void showDownloadToast() =>
_toast.showBottom('appSelectorView.downloadToast');
} }

View File

@@ -13,7 +13,7 @@ class ContributorsView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<ContributorsViewModel>.reactive( return ViewModelBuilder<ContributorsViewModel>.reactive(
viewModelBuilder: () => ContributorsViewModel(), viewModelBuilder: () => ContributorsViewModel(),
onModelReady: (model) => model.getContributors(), onViewModelReady: (model) => model.getContributors(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
body: CustomScrollView( body: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
@@ -23,7 +23,7 @@ class ContributorsView extends StatelessWidget {
child: Text( child: Text(
'', '',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
), ),

View File

@@ -11,7 +11,7 @@ class ContributorsViewModel extends BaseViewModel {
List<dynamic> managerContributors = []; List<dynamic> managerContributors = [];
Future<void> getContributors() async { Future<void> getContributors() async {
Map<String, List<dynamic>> contributors = final Map<String, List<dynamic>> contributors =
await _managerAPI.getContributors(); await _managerAPI.getContributors();
patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? []; patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? [];
patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? []; patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? [];

View File

@@ -1,13 +1,12 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:animations/animations.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/homeView/available_updates_card.dart'; import 'package:revanced_manager/ui/widgets/homeView/available_updates_card.dart';
import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.dart'; import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.dart';
import 'package:revanced_manager/ui/widgets/homeView/latest_commit_card.dart'; import 'package:revanced_manager/ui/widgets/homeView/latest_commit_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_chip.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@@ -18,12 +17,11 @@ class HomeView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<HomeViewModel>.reactive( return ViewModelBuilder<HomeViewModel>.reactive(
disposeViewModel: false, disposeViewModel: false,
onModelReady: (model) => model.initialize(context), fireOnViewModelReadyOnce: true,
onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => locator<HomeViewModel>(), viewModelBuilder: () => locator<HomeViewModel>(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
body: RefreshIndicator( body: RefreshIndicator(
color: Theme.of(context).colorScheme.secondary,
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
onRefresh: () => model.forceRefresh(context), onRefresh: () => model.forceRefresh(context),
child: CustomScrollView( child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
@@ -34,7 +32,7 @@ class HomeView extends StatelessWidget {
child: Text( child: Text(
'', '',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
), ),
@@ -48,40 +46,67 @@ class HomeView extends StatelessWidget {
'homeView.updatesSubtitle', 'homeView.updatesSubtitle',
child: Text( child: Text(
'', '',
style: Theme.of(context).textTheme.headline6!, style: Theme.of(context).textTheme.titleLarge,
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
LatestCommitCard( LatestCommitCard(
onPressed: () => onPressedManager: () =>
model.showUpdateConfirmationDialog(context), model.showUpdateConfirmationDialog(context),
onPressedPatches: () => model.forceRefresh(context),
), ),
const SizedBox(height: 23), const SizedBox(height: 23),
I18nText( I18nText(
'homeView.patchedSubtitle', 'homeView.patchedSubtitle',
child: Text( child: Text(
'', '',
style: Theme.of(context).textTheme.headline6!, style: Theme.of(context).textTheme.titleLarge,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: <Widget>[ children: <Widget>[
CustomChip( ActionChip(
avatar: const Icon(Icons.grid_view),
label: I18nText('homeView.installed'), label: I18nText('homeView.installed'),
isSelected: !model.showUpdatableApps, side: BorderSide(
onSelected: (value) { color: model.showUpdatableApps
? Theme.of(context).colorScheme.outline
: Theme.of(context)
.colorScheme
.secondaryContainer,
width: model.showUpdatableApps ? 1 : 1,
),
backgroundColor: model.showUpdatableApps
? Theme.of(context).colorScheme.background
: Theme.of(context)
.colorScheme
.secondaryContainer,
onPressed: () {
model.toggleUpdatableApps(false); model.toggleUpdatableApps(false);
}, },
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
CustomChip( ActionChip(
avatar: const Icon(Icons.update),
label: I18nText('homeView.updatesAvailable'), label: I18nText('homeView.updatesAvailable'),
isSelected: model.showUpdatableApps, side: BorderSide(
onSelected: (value) { color: !model.showUpdatableApps
? Theme.of(context).colorScheme.outline
: Theme.of(context)
.colorScheme
.secondaryContainer,
width: !model.showUpdatableApps ? 1 : 1,
),
backgroundColor: !model.showUpdatableApps
? Theme.of(context).colorScheme.background
: Theme.of(context)
.colorScheme
.secondaryContainer,
onPressed: () {
model.toggleUpdatableApps(true); model.toggleUpdatableApps(true);
}, },
) ),
], ],
), ),
const SizedBox(height: 14), const SizedBox(height: 14),

View File

@@ -1,26 +1,30 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:app_installer/app_installer.dart'; import 'package:app_installer/app_installer.dart';
import 'package:cross_connectivity/cross_connectivity.dart'; import 'package:cross_connectivity/cross_connectivity.dart';
import 'package:device_apps/device_apps.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart'; import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_dialog.dart'; import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_dialog.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/about_info.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:timezone/timezone.dart' as tz;
@lazySingleton @lazySingleton
class HomeViewModel extends BaseViewModel { class HomeViewModel extends BaseViewModel {
@@ -28,29 +32,55 @@ class HomeViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>(); final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final GithubAPI _githubAPI = locator<GithubAPI>(); final GithubAPI _githubAPI = locator<GithubAPI>();
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
DateTime? _lastUpdate; DateTime? _lastUpdate;
bool showUpdatableApps = false; bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = []; List<PatchedApplication> patchedInstalledApps = [];
List<PatchedApplication> patchedUpdatableApps = []; List<PatchedApplication> patchedUpdatableApps = [];
String _managerVersion = '';
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
_managerVersion = await AboutInfo.getInfo().then(
(value) => value.keys.contains('version') ? value['version']! : '',
);
_managerVersion = await _managerAPI.getCurrentManagerVersion();
await flutterLocalNotificationsPlugin.initialize( await flutterLocalNotificationsPlugin.initialize(
const InitializationSettings( const InitializationSettings(
android: AndroidInitializationSettings('ic_notification'), android: AndroidInitializationSettings('ic_notification'),
), ),
onSelectNotification: (p) => onDidReceiveNotificationResponse: (response) async {
DeviceApps.openApp('app.revanced.manager.flutter'), if (response.id == 0) {
_toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
await AppInstaller.installApk(managerApk.path);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
}
},
); );
flutterLocalNotificationsPlugin flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation< .resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>() AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission(); ?.requestPermission();
bool isConnected = await Connectivity().checkConnection(); final bool isConnected = await Connectivity().checkConnection();
if (!isConnected) { if (!isConnected) {
_toast.showBottom('homeView.noConnection'); _toast.showBottom('homeView.noConnection');
} }
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
_toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
await AppInstaller.installApk(managerApk.path);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
}
_getPatchedApps(); _getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps()); _managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
} }
@@ -67,7 +97,7 @@ class HomeViewModel extends BaseViewModel {
notifyListeners(); notifyListeners();
} }
void navigateToPatcher(PatchedApplication app) async { Future<void> navigateToPatcher(PatchedApplication app) async {
locator<PatcherViewModel>().selectedApp = app; locator<PatcherViewModel>().selectedApp = app;
locator<PatcherViewModel>().selectedPatches = locator<PatcherViewModel>().selectedPatches =
await _patcherAPI.getAppliedPatches(app.appliedPatches); await _patcherAPI.getAppliedPatches(app.appliedPatches);
@@ -76,10 +106,7 @@ class HomeViewModel extends BaseViewModel {
} }
void _getPatchedApps() { void _getPatchedApps() {
patchedInstalledApps = _managerAPI patchedInstalledApps = _managerAPI.getPatchedApps().toList();
.getPatchedApps()
.where((app) => app.hasUpdates == false)
.toList();
patchedUpdatableApps = _managerAPI patchedUpdatableApps = _managerAPI
.getPatchedApps() .getPatchedApps()
.where((app) => app.hasUpdates == true) .where((app) => app.hasUpdates == true)
@@ -88,59 +115,158 @@ class HomeViewModel extends BaseViewModel {
} }
Future<bool> hasManagerUpdates() async { Future<bool> hasManagerUpdates() async {
String? latestVersion = await _managerAPI.getLatestManagerVersion(); final String? latestVersion = await _managerAPI.getLatestManagerVersion();
String currentVersion = await _managerAPI.getCurrentManagerVersion(); String currentVersion = await _managerAPI.getCurrentManagerVersion();
// add v to current version
if (!currentVersion.startsWith('v')) {
currentVersion = 'v$currentVersion';
}
if (latestVersion != currentVersion) {
return true;
}
return false;
}
Future<bool> hasPatchesUpdates() async {
final String? latestVersion = await _managerAPI.getLatestPatchesVersion();
final String? currentVersion = await _managerAPI.getCurrentPatchesVersion();
if (latestVersion != null) { if (latestVersion != null) {
try { try {
int latestVersionInt = final int latestVersionInt =
int.parse(latestVersion.replaceAll(RegExp('[^0-9]'), '')); int.parse(latestVersion.replaceAll(RegExp('[^0-9]'), ''));
int currentVersionInt = final int currentVersionInt =
int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), '')); int.parse(currentVersion!.replaceAll(RegExp('[^0-9]'), ''));
return latestVersionInt > currentVersionInt; return latestVersionInt > currentVersionInt;
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
return false; return false;
} }
} }
return false; return false;
} }
Future<File?> downloadManager() async {
try {
final response = await _revancedAPI.downloadManager();
final bytes = await response!.readAsBytes();
final tempDir = await getTemporaryDirectory();
final tempFile = File('${tempDir.path}/revanced-manager.apk');
await tempFile.writeAsBytes(bytes);
return tempFile;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
Future<void> updateManager(BuildContext context) async { Future<void> updateManager(BuildContext context) async {
try { try {
_toast.showBottom('homeView.downloadingMessage'); _toast.showBottom('homeView.downloadingMessage');
File? managerApk = await _managerAPI.downloadManager(); showDialog(
if (managerApk != null) { context: context,
await flutterLocalNotificationsPlugin.zonedSchedule( builder: (context) => SimpleDialog(
0, contentPadding: const EdgeInsets.all(16.0),
FlutterI18n.translate( title: I18nText(
context, 'homeView.downloadingMessage',
'homeView.notificationTitle', child: Text(
), '',
FlutterI18n.translate( style: TextStyle(
context, fontSize: 20,
'homeView.notificationText', fontWeight: FontWeight.w500,
), color: Theme.of(context).colorScheme.secondary,
tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)), ),
const NotificationDetails(
android: AndroidNotificationDetails(
'revanced_manager_channel',
'ReVanced Manager Channel',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
), ),
), ),
androidAllowWhileIdle: true, children: [
uiLocalNotificationDateInterpretation: Column(
UILocalNotificationDateInterpretation.absoluteTime, children: [
); Row(
children: [
Icon(
Icons.new_releases_outlined,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 8.0),
Text(
'v$_managerVersion',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
const SizedBox(height: 16.0),
StreamBuilder<double>(
initialData: 0.0,
stream: _revancedAPI.managerUpdateProgress.stream,
builder: (context, snapshot) {
return LinearProgressIndicator(
value: snapshot.data! * 0.01,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.secondary,
),
);
},
),
const SizedBox(height: 16.0),
Align(
alignment: Alignment.centerRight,
child: CustomMaterialButton(
label: I18nText('cancelButton'),
onPressed: () {
_revancedAPI.disposeManagerUpdateProgress();
Navigator.of(context).pop();
},
),
),
],
),
],
),
);
final File? managerApk = await downloadManager();
if (managerApk != null) {
// await flutterLocalNotificationsPlugin.zonedSchedule(
// 0,
// FlutterI18n.translate(
// context,
// 'homeView.notificationTitle',
// ),
// FlutterI18n.translate(
// context,
// 'homeView.notificationText',
// ),
// tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
// const NotificationDetails(
// android: AndroidNotificationDetails(
// 'revanced_manager_channel',
// 'ReVanced Manager Channel',
// importance: Importance.max,
// priority: Priority.high,
// ticker: 'ticker',
// ),
// ),
// androidAllowWhileIdle: true,
// uiLocalNotificationDateInterpretation:
// UILocalNotificationDateInterpretation.absoluteTime,
// );
_toast.showBottom('homeView.installingMessage'); _toast.showBottom('homeView.installingMessage');
await AppInstaller.installApk(managerApk.path); await AppInstaller.installApk(managerApk.path);
} else { } else {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
_toast.showBottom('homeView.errorInstallMessage'); _toast.showBottom('homeView.errorInstallMessage');
} }
} }
@@ -149,7 +275,9 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.updatesDisabled'); _toast.showBottom('homeView.updatesDisabled');
} }
Future<void> showUpdateConfirmationDialog(BuildContext parentContext) { Future<void> showUpdateConfirmationDialog(
BuildContext parentContext,
) {
return showModalBottomSheet( return showModalBottomSheet(
context: parentContext, context: parentContext,
isScrollControlled: true, isScrollControlled: true,
@@ -178,6 +306,7 @@ class HomeViewModel extends BaseViewModel {
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) { _lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
_managerAPI.clearAllData(); _managerAPI.clearAllData();
} }
_toast.showBottom('homeView.refreshSuccess');
initialize(context); initialize(context);
} }
} }

View File

@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart'; import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart'; import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@@ -15,7 +15,7 @@ class InstallerView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<InstallerViewModel>.reactive( return ViewModelBuilder<InstallerViewModel>.reactive(
onModelReady: (model) => model.initialize(context), onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => InstallerViewModel(), viewModelBuilder: () => InstallerViewModel(),
builder: (context, model, child) => WillPopScope( builder: (context, model, child) => WillPopScope(
child: SafeArea( child: SafeArea(
@@ -28,7 +28,7 @@ class InstallerView extends StatelessWidget {
title: Text( title: Text(
model.headerLogs, model.headerLogs,
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
onBackButtonPressed: () => model.onWillPop(context), onBackButtonPressed: () => model.onWillPop(context),
@@ -48,15 +48,15 @@ class InstallerView extends StatelessWidget {
), ),
), ),
), ),
1: I18nText( 1: I18nText(
'installerView.exportApkMenuOption', 'installerView.exportApkMenuOption',
child: const Text( child: const Text(
'', '',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
),
), ),
), ),
),
2: I18nText( 2: I18nText(
'installerView.shareLogMenuOption', 'installerView.shareLogMenuOption',
child: const Text( child: const Text(
@@ -71,9 +71,9 @@ class InstallerView extends StatelessWidget {
), ),
], ],
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size(double.infinity, 1.0), preferredSize: const Size(double.infinity, 1.0),
child: child: GradientProgressIndicator(progress: model.progress),
GradientProgressIndicator(progress: model.progress!)), ),
), ),
SliverPadding( SliverPadding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),

View File

@@ -1,5 +1,6 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_background/flutter_background.dart'; import 'package:flutter_background/flutter_background.dart';
@@ -14,7 +15,6 @@ import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
@@ -51,16 +51,15 @@ class InstallerViewModel extends BaseViewModel {
context, context,
'installerView.notificationText', 'installerView.notificationText',
), ),
notificationImportance: AndroidNotificationImportance.Default,
notificationIcon: const AndroidResource( notificationIcon: const AndroidResource(
name: 'ic_notification', name: 'ic_notification',
defType: 'drawable',
), ),
), ),
).then((value) => FlutterBackground.enableBackgroundExecution()); ).then((value) => FlutterBackground.enableBackgroundExecution());
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
// ignore print(e);
} // ignore
} }
} }
await Wakelock.enable(); await Wakelock.enable();
@@ -73,10 +72,10 @@ class InstallerViewModel extends BaseViewModel {
switch (call.method) { switch (call.method) {
case 'update': case 'update':
if (call.arguments != null) { if (call.arguments != null) {
Map<dynamic, dynamic> arguments = call.arguments; final Map<dynamic, dynamic> arguments = call.arguments;
double progress = arguments['progress']; final double progress = arguments['progress'];
String header = arguments['header']; final String header = arguments['header'];
String log = arguments['log']; final String log = arguments['log'];
update(progress, header, log); update(progress, header, log);
} }
break; break;
@@ -133,14 +132,15 @@ class InstallerViewModel extends BaseViewModel {
_app.apkFilePath, _app.apkFilePath,
_patches, _patches,
); );
} on Exception catch (e, s) { } on Exception catch (e) {
update( update(
-100.0, -100.0,
'Aborting...', 'Aborting...',
'An error occurred! Aborting\nError:\n$e', 'An error occurred! Aborting\nError:\n$e',
); );
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
throw await Sentry.captureException(e, stackTrace: s); print(e);
}
} }
} else { } else {
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting'); update(-100.0, 'Aborting...', 'No app or patches selected! Aborting');
@@ -148,24 +148,28 @@ class InstallerViewModel extends BaseViewModel {
if (FlutterBackground.isBackgroundExecutionEnabled) { if (FlutterBackground.isBackgroundExecutionEnabled) {
try { try {
FlutterBackground.disableBackgroundExecution(); FlutterBackground.disableBackgroundExecution();
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
// ignore print(e);
} // ignore
} }
} }
await Wakelock.disable(); await Wakelock.disable();
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }
void installResult(BuildContext context, bool installAsRoot) async { Future<void> installResult(BuildContext context, bool installAsRoot) async {
try { try {
_app.isRooted = installAsRoot; _app.isRooted = installAsRoot;
bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support')); final bool hasMicroG =
bool rootMicroG = installAsRoot && hasMicroG; _patches.any((p) => p.name.endsWith('microg-support'));
bool rootFromStorage = installAsRoot && _app.isFromStorage; final bool rootMicroG = installAsRoot && hasMicroG;
bool ytWithoutRootMicroG = final bool rootFromStorage = installAsRoot && _app.isFromStorage;
final bool ytWithoutRootMicroG =
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube'); !installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) { if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
return showDialog( return showDialog(
@@ -212,24 +216,30 @@ class InstallerViewModel extends BaseViewModel {
await _managerAPI.savePatchedApp(_app); await _managerAPI.savePatchedApp(_app);
} }
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }
void exportResult() { void exportResult() {
try { try {
_patcherAPI.exportPatchedFile(_app.name, _app.version); _patcherAPI.exportPatchedFile(_app.name, _app.version);
} on Exception catch (e, s) { } on Exception catch (e) {
Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }
void shareResult() { void shareResult() {
try { try {
_patcherAPI.sharePatchedFile(_app.name, _app.version); _patcherAPI.sharePatchedFile(_app.name, _app.version);
} on Exception catch (e, s) { } on Exception catch (e) {
Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }
@@ -243,8 +253,10 @@ class InstallerViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp = null; locator<PatcherViewModel>().selectedApp = null;
locator<PatcherViewModel>().selectedPatches.clear(); locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().notifyListeners(); locator<PatcherViewModel>().notifyListeners();
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
print(e);
}
} }
} }

View File

@@ -11,7 +11,7 @@ class NavigationView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<NavigationViewModel>.reactive( return ViewModelBuilder<NavigationViewModel>.reactive(
onModelReady: (model) => model.initialize(context), onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => locator<NavigationViewModel>(), viewModelBuilder: () => locator<NavigationViewModel>(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
body: PageTransitionSwitcher( body: PageTransitionSwitcher(
@@ -42,7 +42,6 @@ class NavigationView extends StatelessWidget {
context, context,
'navigationView.dashboardTab', 'navigationView.dashboardTab',
), ),
tooltip: '',
), ),
NavigationDestination( NavigationDestination(
icon: model.isIndexSelected(1) icon: model.isIndexSelected(1)
@@ -52,7 +51,6 @@ class NavigationView extends StatelessWidget {
context, context,
'navigationView.patcherTab', 'navigationView.patcherTab',
), ),
tooltip: '',
), ),
NavigationDestination( NavigationDestination(
icon: model.isIndexSelected(2) icon: model.isIndexSelected(2)
@@ -62,7 +60,6 @@ class NavigationView extends StatelessWidget {
context, context,
'navigationView.settingsTab', 'navigationView.settingsTab',
), ),
tooltip: '',
), ),
], ],
), ),

View File

@@ -15,19 +15,21 @@ import 'package:stacked/stacked.dart';
@lazySingleton @lazySingleton
class NavigationViewModel extends IndexTrackingViewModel { class NavigationViewModel extends IndexTrackingViewModel {
void initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
locator<Toast>().initialize(context); locator<Toast>().initialize(context);
SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs =
await SharedPreferences.getInstance();
if (prefs.getBool('permissionsRequested') == null) { if (prefs.getBool('permissionsRequested') == null) {
await prefs.setBool('permissionsRequested', true); await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions().then( RootAPI().hasRootPermissions().then(
(value) => Permission.requestInstallPackages.request().then( (value) => Permission.requestInstallPackages.request().then(
(value) => Permission.ignoreBatteryOptimizations.request(), (value) =>
Permission.ignoreBatteryOptimizations.request(),
), ),
); );
} }
if (prefs.getBool('useDarkTheme') == null) { if (prefs.getBool('useDarkTheme') == null) {
bool isDark = final bool isDark =
MediaQuery.of(context).platformBrightness != Brightness.light; MediaQuery.of(context).platformBrightness != Brightness.light;
await prefs.setBool('useDarkTheme', isDark); await prefs.setBool('useDarkTheme', isDark);
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0); await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
@@ -37,7 +39,8 @@ class NavigationViewModel extends IndexTrackingViewModel {
SystemUiOverlayStyle( SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness: systemNavigationBarIconBrightness:
DynamicTheme.of(context)!.theme.brightness == Brightness.light DynamicTheme.of(context)!.theme.brightness ==
Brightness.light
? Brightness.dark ? Brightness.dark
: Brightness.light, : Brightness.light,
), ),

View File

@@ -34,7 +34,7 @@ class PatcherView extends StatelessWidget {
child: Text( child: Text(
'', '',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
), ),

View File

@@ -1,3 +1,5 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
@@ -8,6 +10,7 @@ import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/about_info.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
@@ -40,35 +43,71 @@ class PatcherViewModel extends BaseViewModel {
} }
Future<bool> isValidPatchConfig() async { Future<bool> isValidPatchConfig() async {
bool needsResourcePatching = await _patcherAPI.needsResourcePatching( final bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
selectedPatches, selectedPatches,
); );
if (needsResourcePatching && selectedApp != null) { if (needsResourcePatching && selectedApp != null) {
bool isSplit = await _managerAPI.isSplitApk(selectedApp!); final bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
return !isSplit; return !isSplit;
} }
return true; return true;
} }
Future<void> showPatchConfirmationDialog(BuildContext context) async { Future<void> showPatchConfirmationDialog(BuildContext context) async {
bool isValid = await isValidPatchConfig(); final bool isValid = await isValidPatchConfig();
if (isValid) { if (context.mounted) {
navigateToInstaller(); if (isValid) {
} else { showArmv7WarningDialog(context);
} else {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.armv7WarningDialogText'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
isFilled: false,
onPressed: () {
Navigator.of(context).pop();
showArmv7WarningDialog(context);
},
)
],
),
);
}
}
}
Future<void> showArmv7WarningDialog(BuildContext context) async {
final bool armv7 = await AboutInfo.getInfo().then(
(info) =>
info['arch'] != null &&
info['arch']!.contains('armeabi-v7a') &&
!info['arch']!.contains('arm64-v8a'),
);
if (context.mounted && armv7) {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('warning'), title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.patchDialogText'), content: I18nText('patcherView.armv7WarningDialogText'),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'), label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
CustomMaterialButton( CustomMaterialButton(
label: I18nText('yesButton'), label: I18nText('yesButton'),
isFilled: false,
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
navigateToInstaller(); navigateToInstaller();
@@ -77,6 +116,8 @@ class PatcherViewModel extends BaseViewModel {
], ],
), ),
); );
} else {
navigateToInstaller();
} }
} }
@@ -88,31 +129,31 @@ class PatcherViewModel extends BaseViewModel {
return text; return text;
} }
String getRecommendedVersionString(BuildContext context) { String getSuggestedVersionString(BuildContext context) {
String recommendedVersion = String suggestedVersion =
_patcherAPI.getRecommendedVersion(selectedApp!.packageName); _patcherAPI.getSuggestedVersion(selectedApp!.packageName);
if (recommendedVersion.isEmpty) { if (suggestedVersion.isEmpty) {
recommendedVersion = FlutterI18n.translate( suggestedVersion = FlutterI18n.translate(
context, context,
'appSelectorCard.anyVersion', 'appSelectorCard.anyVersion',
); );
} else { } else {
recommendedVersion = 'v$recommendedVersion'; suggestedVersion = 'v$suggestedVersion';
} }
return '${FlutterI18n.translate( return '${FlutterI18n.translate(
context, context,
'appSelectorCard.currentVersion', 'appSelectorCard.currentVersion',
)}: v${selectedApp!.version}\n${FlutterI18n.translate( )}: v${selectedApp!.version}\n${FlutterI18n.translate(
context, context,
'appSelectorCard.recommendedVersion', 'appSelectorCard.suggestedVersion',
)}: $recommendedVersion'; )}: $suggestedVersion';
} }
Future<void> loadLastSelectedPatches() async { Future<void> loadLastSelectedPatches() async {
this.selectedPatches.clear(); this.selectedPatches.clear();
List<String> selectedPatches = final List<String> selectedPatches =
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName); await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
List<Patch> patches = final List<Patch> patches =
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName); _patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
this this
.selectedPatches .selectedPatches

View File

@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart'; import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_chip.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class PatchesSelectorView extends StatefulWidget { class PatchesSelectorView extends StatefulWidget {
@@ -20,7 +20,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<PatchesSelectorViewModel>.reactive( return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
onModelReady: (model) => model.initialize(), onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => PatchesSelectorViewModel(), viewModelBuilder: () => PatchesSelectorViewModel(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
@@ -45,26 +45,24 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
SliverAppBar( SliverAppBar(
pinned: true, pinned: true,
floating: true, floating: true,
snap: false,
title: I18nText( title: I18nText(
'patchesSelectorView.viewTitle', 'patchesSelectorView.viewTitle',
child: Text( child: Text(
'', '',
style: TextStyle( style: TextStyle(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
), ),
leading: IconButton( leading: IconButton(
icon: Icon( icon: Icon(
Icons.arrow_back, Icons.arrow_back,
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
actions: [ actions: [
Container( Container(
height: 2,
margin: const EdgeInsets.only(top: 12, bottom: 12), margin: const EdgeInsets.only(top: 12, bottom: 12),
padding: padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6), const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
@@ -76,7 +74,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
child: Text( child: Text(
model.patchesVersion!, model.patchesVersion!,
style: TextStyle( style: TextStyle(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
), ),
@@ -103,7 +101,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
horizontal: 12.0, horizontal: 12.0,
), ),
child: SearchBar( child: SearchBar(
showSelectIcon: true,
hintText: FlutterI18n.translate( hintText: FlutterI18n.translate(
context, context,
'patchesSelectorView.searchBarHint', 'patchesSelectorView.searchBarHint',
@@ -113,12 +110,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
_query = searchQuery; _query = searchQuery;
}); });
}, },
onSelectAll: (value) {
if (value) {
model.selectAllPatcherWarning(context);
}
model.selectAllPatches(value);
},
), ),
), ),
), ),
@@ -144,24 +135,24 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
children: [ children: [
Row( Row(
children: [ children: [
CustomChip( ActionChip(
label: label: I18nText('patchesSelectorView.default'),
I18nText('patchesSelectorView.recommended'), tooltip: FlutterI18n.translate(
onSelected: (value) { context,
model.selectRecommendedPatches(); 'patchesSelectorView.defaultTooltip',
),
onPressed: () {
model.selectDefaultPatches();
}, },
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
CustomChip( ActionChip(
label: I18nText('patchesSelectorView.all'),
onSelected: (value) {
model.selectAllPatches(true);
},
),
const SizedBox(width: 8),
CustomChip(
label: I18nText('patchesSelectorView.none'), label: I18nText('patchesSelectorView.none'),
onSelected: (value) { tooltip: FlutterI18n.translate(
context,
'patchesSelectorView.noneTooltip',
),
onPressed: () {
model.clearPatches(); model.clearPatches();
}, },
), ),
@@ -178,7 +169,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
packageVersion: model.getAppVersion(), packageVersion: model.getAppVersion(),
supportedPackageVersions: supportedPackageVersions:
model.getSupportedVersions(patch), model.getSupportedVersions(patch),
isUnsupported: !model.isPatchSupported(patch), isUnsupported: !isPatchSupported(patch),
isSelected: model.isSelected(patch), isSelected: model.isSelected(patch),
onChanged: (value) => onChanged: (value) =>
model.selectPatch(patch, value), model.selectPatch(patch, value),

View File

@@ -1,5 +1,4 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
@@ -8,9 +7,8 @@ import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:flutter/material.dart';
class PatchesSelectorViewModel extends BaseViewModel { class PatchesSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>(); final PatcherAPI _patcherAPI = locator<PatcherAPI>();
@@ -25,10 +23,12 @@ class PatchesSelectorViewModel extends BaseViewModel {
} }
Future<void> initialize() async { Future<void> initialize() async {
getPatchesVersion(); getPatchesVersion().whenComplete(() => notifyListeners());
patches.addAll(_patcherAPI.getFilteredPatches( patches.addAll(
locator<PatcherViewModel>().selectedApp!.originalPackageName, _patcherAPI.getFilteredPatches(
)); locator<PatcherViewModel>().selectedApp!.originalPackageName,
),
);
patches.sort((a, b) => a.name.compareTo(b.name)); patches.sort((a, b) => a.name.compareTo(b.name));
notifyListeners(); notifyListeners();
} }
@@ -48,44 +48,15 @@ class PatchesSelectorViewModel extends BaseViewModel {
notifyListeners(); notifyListeners();
} }
Future<void> selectAllPatcherWarning(BuildContext context) { void selectDefaultPatches() {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patchesSelectorView.selectAllPatchesWarningContent'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
)
],
),
);
}
void selectAllPatches(bool isSelected) {
selectedPatches.clear();
if (isSelected && _managerAPI.areExperimentalPatchesEnabled() == false) {
selectedPatches
.addAll(patches.where((element) => isPatchSupported(element)));
}
if (isSelected && _managerAPI.areExperimentalPatchesEnabled()) {
selectedPatches.addAll(patches);
}
notifyListeners();
}
void selectRecommendedPatches() {
selectedPatches.clear(); selectedPatches.clear();
if (_managerAPI.areExperimentalPatchesEnabled() == false) { if (_managerAPI.areExperimentalPatchesEnabled() == false) {
selectedPatches.addAll(patches.where( selectedPatches.addAll(
(element) => element.excluded == false && isPatchSupported(element))); patches.where(
(element) => element.excluded == false && isPatchSupported(element),
),
);
} }
if (_managerAPI.areExperimentalPatchesEnabled()) { if (_managerAPI.areExperimentalPatchesEnabled()) {
@@ -117,15 +88,18 @@ class PatchesSelectorViewModel extends BaseViewModel {
patchesVersion = await _githubAPI patchesVersion = await _githubAPI
.getLastestReleaseVersion(_managerAPI.getPatchesRepo()); .getLastestReleaseVersion(_managerAPI.getPatchesRepo());
} }
return null;
} }
List<Patch> getQueriedPatches(String query) { List<Patch> getQueriedPatches(String query) {
return patches return patches
.where((patch) => .where(
query.isEmpty || (patch) =>
query.length < 2 || query.isEmpty ||
patch.name.toLowerCase().contains(query.toLowerCase()) || query.length < 2 ||
patch.getSimpleName().toLowerCase().contains(query.toLowerCase())) patch.name.toLowerCase().contains(query.toLowerCase()) ||
patch.getSimpleName().toLowerCase().contains(query.toLowerCase()),
)
.toList(); .toList();
} }
@@ -134,8 +108,8 @@ class PatchesSelectorViewModel extends BaseViewModel {
} }
List<String> getSupportedVersions(Patch patch) { List<String> getSupportedVersions(Patch patch) {
PatchedApplication app = locator<PatcherViewModel>().selectedApp!; final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
Package? package = patch.compatiblePackages.firstWhereOrNull( final Package? package = patch.compatiblePackages.firstWhereOrNull(
(pack) => pack.name == app.packageName, (pack) => pack.name == app.packageName,
); );
if (package != null) { if (package != null) {
@@ -145,13 +119,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
} }
} }
bool isPatchSupported(Patch patch) {
PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
return patch.compatiblePackages.isEmpty || patch.compatiblePackages.any((pack) =>
pack.name == app.packageName &&
(pack.versions.isEmpty || pack.versions.contains(app.version)));
}
void onMenuSelection(value) { void onMenuSelection(value) {
switch (value) { switch (value) {
case 0: case 0:
@@ -161,20 +128,23 @@ class PatchesSelectorViewModel extends BaseViewModel {
} }
Future<void> saveSelectedPatches() async { Future<void> saveSelectedPatches() async {
List<String> selectedPatches = final List<String> selectedPatches =
this.selectedPatches.map((patch) => patch.name).toList(); this.selectedPatches.map((patch) => patch.name).toList();
await _managerAPI.setSelectedPatches( await _managerAPI.setSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName, locator<PatcherViewModel>().selectedApp!.originalPackageName,
selectedPatches); selectedPatches,
);
} }
Future<void> loadSelectedPatches() async { Future<void> loadSelectedPatches() async {
List<String> selectedPatches = await _managerAPI.getSelectedPatches( final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName); locator<PatcherViewModel>().selectedApp!.originalPackageName,
);
if (selectedPatches.isNotEmpty) { if (selectedPatches.isNotEmpty) {
this.selectedPatches.clear(); this.selectedPatches.clear();
this.selectedPatches.addAll( this.selectedPatches.addAll(
patches.where((patch) => selectedPatches.contains(patch.name))); patches.where((patch) => selectedPatches.contains(patch.name)),
);
} else { } else {
locator<Toast>().showBottom('patchesSelectorView.noSavedPatches'); locator<Toast>().showBottom('patchesSelectorView.noSavedPatches');
} }

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
@@ -11,11 +12,12 @@ import 'package:stacked/stacked.dart';
class SManageApiUrl extends BaseViewModel { class SManageApiUrl extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final Toast _toast = locator<Toast>();
final TextEditingController _apiUrlController = TextEditingController(); final TextEditingController _apiUrlController = TextEditingController();
Future<void> showApiUrlDialog(BuildContext context) async { Future<void> showApiUrlDialog(BuildContext context) async {
String apiUrl = _managerAPI.getApiUrl(); final String apiUrl = _managerAPI.getApiUrl();
_apiUrlController.text = apiUrl.replaceAll('https://', ''); _apiUrlController.text = apiUrl.replaceAll('https://', '');
return showDialog( return showDialog(
context: context, context: context,
@@ -90,7 +92,7 @@ class SManageApiUrl extends BaseViewModel {
label: I18nText('yesButton'), label: I18nText('yesButton'),
onPressed: () { onPressed: () {
_managerAPI.setApiUrl(''); _managerAPI.setApiUrl('');
Navigator.of(context).pop(); _toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
) )

View File

@@ -0,0 +1,86 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class SManageKeystorePassword extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final TextEditingController _keystorePasswordController =
TextEditingController();
Future<void> showKeystoreDialog(BuildContext context) async {
final String keystorePasswordText = _managerAPI.getKeystorePassword();
_keystorePasswordController.text = keystorePasswordText;
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: <Widget>[
I18nText('settingsView.selectKeystorePassword'),
const Spacer(),
IconButton(
icon: const Icon(Icons.manage_history_outlined),
onPressed: () => _keystorePasswordController.text =
_managerAPI.defaultKeystorePassword,
color: Theme.of(context).colorScheme.secondary,
)
],
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: Column(
children: <Widget>[
CustomTextField(
inputController: _keystorePasswordController,
label: I18nText('settingsView.selectKeystorePassword'),
hint: '',
onChanged: (value) => notifyListeners(),
),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_keystorePasswordController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
final String passwd = _keystorePasswordController.text;
_managerAPI.setKeystorePassword(passwd);
Navigator.of(context).pop();
},
)
],
),
);
}
}
final sManageKeystorePassword = SManageKeystorePassword();
class SManageKeystorePasswordUI extends StatelessWidget {
const SManageKeystorePasswordUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.selectKeystorePassword',
subtitle: 'settingsView.selectKeystorePasswordHint',
onTap: () => sManageKeystorePassword.showKeystoreDialog(context),
);
}
}

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
@@ -11,6 +12,7 @@ import 'package:stacked/stacked.dart';
class SManageSources extends BaseViewModel { class SManageSources extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final Toast _toast = locator<Toast>();
final TextEditingController _hostSourceController = TextEditingController(); final TextEditingController _hostSourceController = TextEditingController();
final TextEditingController _orgPatSourceController = TextEditingController(); final TextEditingController _orgPatSourceController = TextEditingController();
@@ -19,9 +21,9 @@ class SManageSources extends BaseViewModel {
final TextEditingController _intSourceController = TextEditingController(); final TextEditingController _intSourceController = TextEditingController();
Future<void> showSourcesDialog(BuildContext context) async { Future<void> showSourcesDialog(BuildContext context) async {
String hostRepository = _managerAPI.getRepoUrl(); final String hostRepository = _managerAPI.getRepoUrl();
String patchesRepo = _managerAPI.getPatchesRepo(); final String patchesRepo = _managerAPI.getPatchesRepo();
String integrationsRepo = _managerAPI.getIntegrationsRepo(); final String integrationsRepo = _managerAPI.getIntegrationsRepo();
_hostSourceController.text = hostRepository; _hostSourceController.text = hostRepository;
_orgPatSourceController.text = patchesRepo.split('/')[0]; _orgPatSourceController.text = patchesRepo.split('/')[0];
_patSourceController.text = patchesRepo.split('/')[1]; _patSourceController.text = patchesRepo.split('/')[1];
@@ -117,13 +119,14 @@ class SManageSources extends BaseViewModel {
CustomMaterialButton( CustomMaterialButton(
label: I18nText('okButton'), label: I18nText('okButton'),
onPressed: () { onPressed: () {
_managerAPI.setRepoUrl(_hostSourceController.text); _managerAPI.setRepoUrl(_hostSourceController.text.trim());
_managerAPI.setPatchesRepo( _managerAPI.setPatchesRepo(
'${_orgPatSourceController.text}/${_patSourceController.text}', '${_orgPatSourceController.text.trim()}/${_patSourceController.text.trim()}',
); );
_managerAPI.setIntegrationsRepo( _managerAPI.setIntegrationsRepo(
'${_orgIntSourceController.text}/${_intSourceController.text}', '${_orgIntSourceController.text.trim()}/${_intSourceController.text.trim()}',
); );
_toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
) )
@@ -151,9 +154,10 @@ class SManageSources extends BaseViewModel {
_managerAPI.setRepoUrl(''); _managerAPI.setRepoUrl('');
_managerAPI.setPatchesRepo(''); _managerAPI.setPatchesRepo('');
_managerAPI.setIntegrationsRepo(''); _managerAPI.setIntegrationsRepo('');
Navigator.of(context).pop(); _toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context).pop(); Navigator.of(context)
Navigator.of(context).pop(); ..pop()
..pop();
}, },
) )
], ],

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/main.dart'; import 'package:revanced_manager/main.dart';
import 'package:revanced_manager/services/crowdin_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
@@ -16,7 +15,6 @@ import 'package:timeago/timeago.dart' as timeago;
final _settingViewModel = SettingsViewModel(); final _settingViewModel = SettingsViewModel();
class SUpdateLanguage extends BaseViewModel { class SUpdateLanguage extends BaseViewModel {
final CrowdinAPI _crowdinAPI = locator<CrowdinAPI>();
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
late SharedPreferences _prefs; late SharedPreferences _prefs;
String selectedLanguage = 'English'; String selectedLanguage = 'English';
@@ -43,7 +41,6 @@ class SUpdateLanguage extends BaseViewModel {
} }
Future<void> initLang() async { Future<void> initLang() async {
languages = await _crowdinAPI.getLanguages();
languages.sort((a, b) => a['name'].compareTo(b['name'])); languages.sort((a, b) => a['name'].compareTo(b['name']));
notifyListeners(); notifyListeners();
} }

View File

@@ -7,7 +7,6 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@@ -23,9 +22,9 @@ class SUpdateTheme extends BaseViewModel {
return _managerAPI.getUseDynamicTheme(); return _managerAPI.getUseDynamicTheme();
} }
void setUseDynamicTheme(BuildContext context, bool value) async { Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value); await _managerAPI.setUseDynamicTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId; final int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme.isEven) { if (currentTheme.isEven) {
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0); await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else { } else {
@@ -38,9 +37,9 @@ class SUpdateTheme extends BaseViewModel {
return _managerAPI.getUseDarkTheme(); return _managerAPI.getUseDarkTheme();
} }
void setUseDarkTheme(BuildContext context, bool value) async { Future<void> setUseDarkTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDarkTheme(value); await _managerAPI.setUseDarkTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId; final int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme < 2) { if (currentTheme < 2) {
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0); await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
} else { } else {
@@ -64,8 +63,8 @@ class SUpdateThemeUI extends StatelessWidget {
return SettingsSection( return SettingsSection(
title: 'settingsView.appearanceSectionTitle', title: 'settingsView.appearanceSectionTitle',
children: <Widget>[ children: <Widget>[
CustomSwitchTile( SwitchListTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.darkThemeLabel', 'settingsView.darkThemeLabel',
child: const Text( child: const Text(
@@ -78,7 +77,7 @@ class SUpdateThemeUI extends StatelessWidget {
), ),
subtitle: I18nText('settingsView.darkThemeHint'), subtitle: I18nText('settingsView.darkThemeHint'),
value: SUpdateTheme().getDarkThemeStatus(), value: SUpdateTheme().getDarkThemeStatus(),
onTap: (value) => SUpdateTheme().setUseDarkTheme( onChanged: (value) => SUpdateTheme().setUseDarkTheme(
context, context,
value, value,
), ),
@@ -88,8 +87,8 @@ class SUpdateThemeUI extends StatelessWidget {
builder: (context, snapshot) => Visibility( builder: (context, snapshot) => Visibility(
visible: visible:
snapshot.hasData && snapshot.data! >= ANDROID_12_SDK_VERSION, snapshot.hasData && snapshot.data! >= ANDROID_12_SDK_VERSION,
child: CustomSwitchTile( child: SwitchListTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.dynamicThemeLabel', 'settingsView.dynamicThemeLabel',
child: const Text( child: const Text(
@@ -102,11 +101,12 @@ class SUpdateThemeUI extends StatelessWidget {
), ),
subtitle: I18nText('settingsView.dynamicThemeHint'), subtitle: I18nText('settingsView.dynamicThemeHint'),
value: _settingViewModel.sUpdateTheme.getDynamicThemeStatus(), value: _settingViewModel.sUpdateTheme.getDynamicThemeStatus(),
onTap: (value) => onChanged: (value) => {
_settingViewModel.sUpdateTheme.setUseDynamicTheme( _settingViewModel.sUpdateTheme.setUseDynamicTheme(
context, context,
value, value,
), ),
},
), ),
), ),
), ),

View File

@@ -3,8 +3,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_update_language.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_update_theme.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart';
@@ -33,7 +32,7 @@ class SettingsView extends StatelessWidget {
child: Text( child: Text(
'', '',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
), ),
@@ -50,8 +49,6 @@ class SettingsView extends StatelessWidget {
_settingsDivider, _settingsDivider,
SExportSection(), SExportSection(),
_settingsDivider, _settingsDivider,
// SLoggingSection(),
// _settingsDivider,
SInfoSection(), SInfoSection(),
], ],
), ),

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:cr_file_saver/file_saver.dart'; import 'package:cr_file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:logcat/logcat.dart'; import 'package:logcat/logcat.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
@@ -9,15 +10,15 @@ import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_update_language.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_update_theme.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:share_extend/share_extend.dart'; import 'package:share_extend/share_extend.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
class SettingsViewModel extends BaseViewModel { class SettingsViewModel extends BaseViewModel {
final NavigationService _navigationService = locator<NavigationService>(); final NavigationService _navigationService =
locator<NavigationService>();
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
@@ -28,16 +29,6 @@ class SettingsViewModel extends BaseViewModel {
_navigationService.navigateTo(Routes.contributorsView); _navigationService.navigateTo(Routes.contributorsView);
} }
bool isSentryEnabled() {
return _managerAPI.isSentryEnabled();
}
void useSentry(bool value) {
_managerAPI.setSentryStatus(value);
_toast.showBottom('settingsView.restartAppForChanges');
notifyListeners();
}
bool areUniversalPatchesEnabled() { bool areUniversalPatchesEnabled() {
return _managerAPI.areUniversalPatchesEnabled(); return _managerAPI.areUniversalPatchesEnabled();
} }
@@ -70,47 +61,91 @@ class SettingsViewModel extends BaseViewModel {
Future<void> exportPatches() async { Future<void> exportPatches() async {
try { try {
File outFile = File(_managerAPI.storedPatchesFile); final File outFile = File(_managerAPI.storedPatchesFile);
if (outFile.existsSync()) { if (outFile.existsSync()) {
String dateTime = final String dateTime = DateTime.now()
DateTime.now().toString().replaceAll(' ', '_').split('.').first; .toString()
String tempFilePath = .replaceAll(' ', '_')
'${outFile.path.substring(0, outFile.path.lastIndexOf('/') + 1)}selected_patches_$dateTime.json'; .split('.')
outFile.copySync(tempFilePath); .first;
await CRFileSaver.saveFileWithDialog(SaveFileDialogParams( await CRFileSaver.saveFileWithDialog(
sourceFilePath: tempFilePath, destinationFileName: '')); SaveFileDialogParams(
File(tempFilePath).delete(); sourceFilePath: outFile.path,
locator<Toast>().showBottom('settingsView.exportedPatches'); destinationFileName: 'selected_patches_$dateTime.json',
),
);
_toast.showBottom('settingsView.exportedPatches');
} else { } else {
locator<Toast>().showBottom('settingsView.noExportFileFound'); _toast.showBottom('settingsView.noExportFileFound');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} }
} on Exception catch (e, s) {
Sentry.captureException(e, stackTrace: s);
} }
} }
Future<void> importPatches() async { Future<void> importPatches() async {
try { try {
FilePickerResult? result = await FilePicker.platform.pickFiles( final FilePickerResult? result =
await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowedExtensions: ['json'], allowedExtensions: ['json'],
); );
if (result != null && result.files.single.path != null) { if (result != null && result.files.single.path != null) {
File inFile = File(result.files.single.path!); final File inFile = File(result.files.single.path!);
final File storedPatchesFile = File(_managerAPI.storedPatchesFile); inFile.copySync(_managerAPI.storedPatchesFile);
if (!storedPatchesFile.existsSync()) {
storedPatchesFile.createSync(recursive: true);
}
inFile.copySync(storedPatchesFile.path);
inFile.delete(); inFile.delete();
if (locator<PatcherViewModel>().selectedApp != null) { if (locator<PatcherViewModel>().selectedApp != null) {
locator<PatcherViewModel>().loadLastSelectedPatches(); locator<PatcherViewModel>().loadLastSelectedPatches();
} }
locator<Toast>().showBottom('settingsView.importedPatches'); _toast.showBottom('settingsView.importedPatches');
} }
} on Exception catch (e, s) { } on Exception catch (e) {
await Sentry.captureException(e, stackTrace: s); if (kDebugMode) {
locator<Toast>().showBottom('settingsView.jsonSelectorErrorMessage'); print(e);
}
_toast.showBottom('settingsView.jsonSelectorErrorMessage');
}
}
Future<void> exportKeystore() async {
try {
final File outFile = File(_managerAPI.keystoreFile);
if (outFile.existsSync()) {
final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: outFile.path,
destinationFileName: 'keystore_$dateTime.keystore',
),
);
_toast.showBottom('settingsView.exportedKeystore');
} else {
_toast.showBottom('settingsView.noKeystoreExportFileFound');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> importKeystore() async {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
final File inFile = File(result.files.single.path!);
inFile.copySync(_managerAPI.keystoreFile);
_toast.showBottom('settingsView.importedKeystore');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
_toast.showBottom('settingsView.keystoreSelectorErrorMessage');
} }
} }
@@ -120,13 +155,13 @@ class SettingsViewModel extends BaseViewModel {
} }
Future<int> getSdkVersion() async { Future<int> getSdkVersion() async {
AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo; final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt ?? -1; return info.version.sdkInt;
} }
Future<void> deleteLogs() async { Future<void> deleteLogs() async {
Directory appCacheDir = await getTemporaryDirectory(); final Directory appCacheDir = await getTemporaryDirectory();
Directory logsDir = Directory('${appCacheDir.path}/logs'); final Directory logsDir = Directory('${appCacheDir.path}/logs');
if (logsDir.existsSync()) { if (logsDir.existsSync()) {
logsDir.deleteSync(recursive: true); logsDir.deleteSync(recursive: true);
} }
@@ -134,17 +169,18 @@ class SettingsViewModel extends BaseViewModel {
} }
Future<void> exportLogcatLogs() async { Future<void> exportLogcatLogs() async {
Directory appCache = await getTemporaryDirectory(); final Directory appCache = await getTemporaryDirectory();
Directory logDir = Directory('${appCache.path}/logs'); final Directory logDir = Directory('${appCache.path}/logs');
logDir.createSync(); logDir.createSync();
String dateTime = DateTime.now() final String dateTime = DateTime.now()
.toIso8601String() .toIso8601String()
.replaceAll('-', '') .replaceAll('-', '')
.replaceAll(':', '') .replaceAll(':', '')
.replaceAll('T', '') .replaceAll('T', '')
.replaceAll('.', ''); .replaceAll('.', '');
File logcat = File('${logDir.path}/revanced-manager_logcat_$dateTime.log'); final File logcat =
String logs = await Logcat.execute(); File('${logDir.path}/revanced-manager_logcat_$dateTime.log');
final String logs = await Logcat.execute();
logcat.writeAsStringSync(logs); logcat.writeAsStringSync(logs);
ShareExtend.share(logcat.path, 'file'); ShareExtend.share(logcat.path, 'file');
} }

View File

@@ -8,12 +8,11 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class AppInfoView extends StatelessWidget { class AppInfoView extends StatelessWidget {
final PatchedApplication app;
const AppInfoView({ const AppInfoView({
Key? key, Key? key,
required this.app, required this.app,
}) : super(key: key); }) : super(key: key);
final PatchedApplication app;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -28,7 +27,7 @@ class AppInfoView extends StatelessWidget {
child: Text( child: Text(
'', '',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
), ),
), ),
@@ -52,13 +51,13 @@ class AppInfoView extends StatelessWidget {
Text( Text(
app.name, app.name,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline6, style: Theme.of(context).textTheme.titleLarge,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
app.version, app.version,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1, style: Theme.of(context).textTheme.titleLarge,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Padding( Padding(

View File

@@ -29,7 +29,7 @@ class AppInfoViewModel extends BaseViewModel {
) async { ) async {
bool isUninstalled = true; bool isUninstalled = true;
if (app.isRooted) { if (app.isRooted) {
bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { if (hasRootPermissions) {
await _rootAPI.deleteApp(app.packageName, app.apkFilePath); await _rootAPI.deleteApp(app.packageName, app.apkFilePath);
if (!onlyUnpatch) { if (!onlyUnpatch) {
@@ -45,7 +45,7 @@ class AppInfoViewModel extends BaseViewModel {
} }
} }
void navigateToPatcher(PatchedApplication app) async { Future<void> navigateToPatcher(PatchedApplication app) async {
locator<PatcherViewModel>().selectedApp = app; locator<PatcherViewModel>().selectedApp = app;
locator<PatcherViewModel>().selectedPatches = locator<PatcherViewModel>().selectedPatches =
await _patcherAPI.getAppliedPatches(app.appliedPatches); await _patcherAPI.getAppliedPatches(app.appliedPatches);
@@ -62,7 +62,7 @@ class AppInfoViewModel extends BaseViewModel {
PatchedApplication app, PatchedApplication app,
bool onlyUnpatch, bool onlyUnpatch,
) async { ) async {
bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (app.isRooted && !hasRootPermissions) { if (app.isRooted && !hasRootPermissions) {
return showDialog( return showDialog(
context: context, context: context,
@@ -134,7 +134,8 @@ class AppInfoViewModel extends BaseViewModel {
title: I18nText('appInfoView.appliedPatchesLabel'), title: I18nText('appInfoView.appliedPatchesLabel'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView( content: SingleChildScrollView(
child: Text(getAppliedPatchesString(app.appliedPatches))), child: Text(getAppliedPatchesString(app.appliedPatches)),
),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( CustomMaterialButton(
label: I18nText('okButton'), label: I18nText('okButton'),
@@ -146,13 +147,15 @@ class AppInfoViewModel extends BaseViewModel {
} }
String getAppliedPatchesString(List<String> appliedPatches) { String getAppliedPatchesString(List<String> appliedPatches) {
List<String> names = appliedPatches final List<String> names = appliedPatches
.map((p) => p .map(
.replaceAll('-', ' ') (p) => p
.split('-') .replaceAll('-', ' ')
.join(' ') .split('-')
.toTitleCase() .join(' ')
.replaceFirst('Microg', 'MicroG')) .toTitleCase()
.replaceFirst('Microg', 'MicroG'),
)
.toList(); .toList();
return '\u2022 ${names.join('\n\u2022 ')}'; return '\u2022 ${names.join('\n\u2022 ')}';
} }

View File

@@ -21,7 +21,6 @@ class AppSkeletonLoader extends StatelessWidget {
style: SkeletonAvatarStyle( style: SkeletonAvatarStyle(
width: screenWidth * 0.15, width: screenWidth * 0.15,
height: screenWidth * 0.15, height: screenWidth * 0.15,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
), ),

View File

@@ -3,20 +3,21 @@ import 'package:flutter/material.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class InstalledAppItem extends StatefulWidget { class InstalledAppItem extends StatefulWidget {
final String name;
final String pkgName;
final Uint8List icon;
final int patchesCount;
final Function()? onTap;
const InstalledAppItem({ const InstalledAppItem({
Key? key, Key? key,
required this.name, required this.name,
required this.pkgName, required this.pkgName,
required this.icon, required this.icon,
required this.patchesCount, required this.patchesCount,
required this.suggestedVersion,
this.onTap, this.onTap,
}) : super(key: key); }) : super(key: key);
final String name;
final String pkgName;
final Uint8List icon;
final int patchesCount;
final String suggestedVersion;
final Function()? onTap;
@override @override
State<InstalledAppItem> createState() => _InstalledAppItemState(); State<InstalledAppItem> createState() => _InstalledAppItemState();
@@ -47,32 +48,35 @@ class _InstalledAppItemState extends State<InstalledAppItem> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text(
widget.name,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(widget.pkgName),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: <Widget>[
Text( Text(
widget.name, widget.suggestedVersion.isEmpty
maxLines: 2, ? 'All versions'
overflow: TextOverflow.visible, : widget.suggestedVersion,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(width: 6), const SizedBox(width: 4),
Text( Text(
widget.patchesCount == 1 widget.patchesCount == 1
? "${widget.patchesCount} patch" ? '${widget.patchesCount} patch'
: "${widget.patchesCount} patches", : '${widget.patchesCount} patches',
style: TextStyle( style: TextStyle(
fontSize: 8,
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
), ),
), ),
], ],
), ),
const SizedBox(height: 4),
Text(widget.pkgName),
], ],
), ),
), ),

View File

@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class NotInstalledAppItem extends StatefulWidget {
const NotInstalledAppItem({
Key? key,
required this.name,
required this.patchesCount,
required this.suggestedVersion,
this.onTap,
}) : super(key: key);
final String name;
final int patchesCount;
final String suggestedVersion;
final Function()? onTap;
@override
State<NotInstalledAppItem> createState() => _NotInstalledAppItem();
}
class _NotInstalledAppItem extends State<NotInstalledAppItem> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: CustomCard(
onTap: widget.onTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
height: 48,
padding: const EdgeInsets.symmetric(vertical: 4.0),
alignment: Alignment.center,
child: const CircleAvatar(
backgroundColor: Colors.transparent,
child: Icon(
Icons.square_rounded,
color: Colors.grey,
size: 44,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
const Text('App not installed.'),
const SizedBox(height: 4),
Row(
children: [
Text(
widget.suggestedVersion.isEmpty
? 'All versions'
: widget.suggestedVersion,
),
const SizedBox(width: 4),
Text(
widget.patchesCount == 1
? '${widget.patchesCount} patch'
: '${widget.patchesCount} patches',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
],
),
),
],
),
),
);
}
}

View File

@@ -6,14 +6,13 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class ContributorsCard extends StatefulWidget { class ContributorsCard extends StatefulWidget {
final String title;
final List<dynamic> contributors;
const ContributorsCard({ const ContributorsCard({
Key? key, Key? key,
required this.title, required this.title,
required this.contributors, required this.contributors,
}) : super(key: key); }) : super(key: key);
final String title;
final List<dynamic> contributors;
@override @override
State<ContributorsCard> createState() => _ContributorsCardState(); State<ContributorsCard> createState() => _ContributorsCardState();
@@ -56,6 +55,7 @@ class _ContributorsCardState extends State<ContributorsCard> {
Uri.parse( Uri.parse(
widget.contributors[index]['html_url'], widget.contributors[index]['html_url'],
), ),
mode: LaunchMode.externalApplication,
), ),
child: FutureBuilder<File?>( child: FutureBuilder<File?>(
future: DefaultCacheManager().getSingleFile( future: DefaultCacheManager().getSingleFile(

View File

@@ -3,7 +3,6 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/application_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class AvailableUpdatesCard extends StatelessWidget { class AvailableUpdatesCard extends StatelessWidget {
@@ -29,7 +28,7 @@ class AvailableUpdatesCard extends StatelessWidget {
child: Text( child: Text(
'', '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1!.copyWith( style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
), ),
), ),
@@ -54,7 +53,7 @@ class AvailableUpdatesCard extends StatelessWidget {
// child: Text( // child: Text(
// '', // '',
// textAlign: TextAlign.center, // textAlign: TextAlign.center,
// style: Theme.of(context).textTheme.subtitle1!.copyWith( // style: Theme.of(context).textTheme.titleMedium!.copyWith(
// color: Theme.of(context).colorScheme.secondary, // color: Theme.of(context).colorScheme.secondary,
// ), // ),
// ), // ),

View File

@@ -30,7 +30,7 @@ class InstalledAppsCard extends StatelessWidget {
child: Text( child: Text(
'', '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1!.copyWith( style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
), ),
), ),

View File

@@ -2,16 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
class LatestCommitCard extends StatefulWidget { class LatestCommitCard extends StatefulWidget {
final Function() onPressed;
const LatestCommitCard({ const LatestCommitCard({
Key? key, Key? key,
required this.onPressed, required this.onPressedManager,
required this.onPressedPatches,
}) : super(key: key); }) : super(key: key);
final Function() onPressedManager;
final Function() onPressedPatches;
@override @override
State<LatestCommitCard> createState() => _LatestCommitCardState(); State<LatestCommitCard> createState() => _LatestCommitCardState();
@@ -22,67 +23,109 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomCard( return Column(
child: Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, // ReVanced Manager
children: <Widget>[ CustomCard(
Column( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Row( Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
I18nText('latestCommitCard.patcherLabel'), Row(
FutureBuilder<String?>( children: const <Widget>[
future: model.getLatestPatcherReleaseTime(), Text('ReVanced Manager'),
builder: (context, snapshot) => Text( ],
snapshot.hasData && snapshot.data!.isNotEmpty ),
? FlutterI18n.translate( const SizedBox(height: 4),
context, Row(
'latestCommitCard.timeagoLabel', children: <Widget>[
translationParams: {'time': snapshot.data!}, FutureBuilder<String?>(
) future: model.getLatestManagerReleaseTime(),
: FlutterI18n.translate( builder: (context, snapshot) =>
context, snapshot.hasData && snapshot.data!.isNotEmpty
'latestCommitCard.loadingLabel', ? I18nText(
), 'latestCommitCard.timeagoLabel',
), translationParams: {'time': snapshot.data!},
)
: I18nText('latestCommitCard.loadingLabel'),
),
],
), ),
], ],
), ),
const SizedBox(height: 4), FutureBuilder<bool>(
Row( future: model.hasManagerUpdates(),
children: <Widget>[ initialData: false,
I18nText('latestCommitCard.managerLabel'), builder: (context, snapshot) => Opacity(
FutureBuilder<String?>( opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25,
future: model.getLatestManagerReleaseTime(), child: CustomMaterialButton(
builder: (context, snapshot) => label: I18nText('updateButton'),
snapshot.hasData && snapshot.data!.isNotEmpty onPressed: snapshot.hasData && snapshot.data!
? I18nText( ? widget.onPressedManager
'latestCommitCard.timeagoLabel', : () => {},
translationParams: {'time': snapshot.data!},
)
: I18nText('latestCommitCard.loadingLabel'),
), ),
], ),
), ),
], ],
), ),
FutureBuilder<bool>( ),
future: locator<HomeViewModel>().hasManagerUpdates(),
initialData: false, const SizedBox(height: 16),
builder: (context, snapshot) => Opacity(
opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25, // ReVanced Patches
child: CustomMaterialButton( CustomCard(
isExpanded: false, child: Row(
label: I18nText('latestCommitCard.updateButton'), mainAxisAlignment: MainAxisAlignment.spaceBetween,
onPressed: snapshot.hasData && snapshot.data! children: <Widget>[
? widget.onPressed Column(
: () => {}, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: const <Widget>[
Text('ReVanced Patches'),
],
),
const SizedBox(height: 4),
Row(
children: <Widget>[
FutureBuilder<String?>(
future: model.getLatestPatcherReleaseTime(),
builder: (context, snapshot) => Text(
snapshot.hasData && snapshot.data!.isNotEmpty
? FlutterI18n.translate(
context,
'latestCommitCard.timeagoLabel',
translationParams: {'time': snapshot.data!},
)
: FlutterI18n.translate(
context,
'latestCommitCard.loadingLabel',
),
),
),
],
),
],
), ),
), FutureBuilder<bool>(
future: locator<HomeViewModel>().hasPatchesUpdates(),
initialData: false,
builder: (context, snapshot) => Opacity(
opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25,
child: CustomMaterialButton(
label: I18nText('updateButton'),
onPressed: snapshot.hasData && snapshot.data!
? widget.onPressedPatches
: () => {},
),
),
),
],
), ),
], ),
), ],
); );
} }
} }

View File

@@ -14,15 +14,14 @@ class UpdateConfirmationDialog extends StatelessWidget {
return DraggableScrollableSheet( return DraggableScrollableSheet(
expand: false, expand: false,
initialChildSize: 0.5,
snap: true, snap: true,
snapSizes: const [0.5], snapSizes: const [0.5],
builder: (context, scrollController) => SingleChildScrollView( builder: (_, scrollController) => SingleChildScrollView(
controller: scrollController, controller: scrollController,
child: SafeArea( child: SafeArea(
child: FutureBuilder<Map<String, dynamic>?>( child: FutureBuilder<Map<String, dynamic>?>(
future: model.getLatestManagerRelease(), future: model.getLatestManagerRelease(),
builder: (context, snapshot) { builder: (_, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const SizedBox( return const SizedBox(
height: 300, height: 300,
@@ -37,20 +36,26 @@ class UpdateConfirmationDialog extends StatelessWidget {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 40.0, left: 24.0, right: 24.0, bottom: 32.0), top: 40.0,
left: 24.0,
right: 24.0,
bottom: 32.0,
),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
I18nText( I18nText(
'homeView.updateDialogTitle', 'homeView.updateDialogTitle',
child: const Text( child: const Text(
"", '',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
),
), ),
), ),
const SizedBox(height: 4.0), const SizedBox(height: 4.0),
@@ -58,12 +63,14 @@ class UpdateConfirmationDialog extends StatelessWidget {
children: [ children: [
Icon( Icon(
Icons.new_releases_outlined, Icons.new_releases_outlined,
color: color: Theme.of(context)
Theme.of(context).colorScheme.secondary, .colorScheme
.secondary,
), ),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
Text( Text(
snapshot.data!["tag_name"] ?? "Unknown", snapshot.data!['tag_name'] ??
'Unknown',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -89,31 +96,36 @@ class UpdateConfirmationDialog extends StatelessWidget {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only(left: 24.0, bottom: 12.0), padding:
const EdgeInsets.only(left: 24.0, bottom: 12.0),
child: I18nText( child: I18nText(
'homeView.updateChangelogTitle', 'homeView.updateChangelogTitle',
child: Text( child: Text(
"", '',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSecondaryContainer), .onSecondaryContainer,
),
), ),
), ),
), ),
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 24.0), margin:
const EdgeInsets.symmetric(horizontal: 24.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer, color: Theme.of(context)
.colorScheme
.secondaryContainer,
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12.0),
), ),
child: Markdown( child: Markdown(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
data: snapshot.data!["body"] ?? "", data: snapshot.data!['body'] ?? '',
), ),
), ),
], ],

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class GradientProgressIndicator extends StatefulWidget { class GradientProgressIndicator extends StatefulWidget {
final double? progress;
const GradientProgressIndicator({required this.progress, super.key}); const GradientProgressIndicator({required this.progress, super.key});
final double? progress;
@override @override
State<GradientProgressIndicator> createState() => State<GradientProgressIndicator> createState() =>

View File

@@ -6,12 +6,11 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class AppSelectorCard extends StatelessWidget { class AppSelectorCard extends StatelessWidget {
final Function() onPressed;
const AppSelectorCard({ const AppSelectorCard({
Key? key, Key? key,
required this.onPressed, required this.onPressed,
}) : super(key: key); }) : super(key: key);
final Function() onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -33,40 +32,41 @@ class AppSelectorCard extends StatelessWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
locator<PatcherViewModel>().selectedApp == null if (locator<PatcherViewModel>().selectedApp == null)
? I18nText('appSelectorCard.widgetSubtitle') I18nText('appSelectorCard.widgetSubtitle')
: Row( else
children: <Widget>[ Row(
SizedBox( children: <Widget>[
height: 18.0, SizedBox(
child: ClipOval( height: 18.0,
child: Image.memory( child: ClipOval(
locator<PatcherViewModel>().selectedApp == null child: Image.memory(
? Uint8List(0) locator<PatcherViewModel>().selectedApp == null
: locator<PatcherViewModel>().selectedApp!.icon, ? Uint8List(0)
fit: BoxFit.cover, : locator<PatcherViewModel>().selectedApp!.icon,
), fit: BoxFit.cover,
),
), ),
const SizedBox(width: 6), ),
Text(
locator<PatcherViewModel>()
.getAppSelectionString(),
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
), ),
locator<PatcherViewModel>().selectedApp == null const SizedBox(width: 6),
? Container() Text(
: Column( locator<PatcherViewModel>().getAppSelectionString(),
children: [ style: const TextStyle(fontWeight: FontWeight.w600),
const SizedBox(height: 4),
Text(
locator<PatcherViewModel>()
.getRecommendedVersionString(context),
),
],
), ),
],
),
if (locator<PatcherViewModel>().selectedApp == null)
Container()
else
Column(
children: [
const SizedBox(height: 4),
Text(
locator<PatcherViewModel>()
.getSuggestedVersionString(context),
),
],
),
], ],
), ),
); );

View File

@@ -6,12 +6,11 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class PatchSelectorCard extends StatelessWidget { class PatchSelectorCard extends StatelessWidget {
final Function() onPressed;
const PatchSelectorCard({ const PatchSelectorCard({
Key? key, Key? key,
required this.onPressed, required this.onPressed,
}) : super(key: key); }) : super(key: key);
final Function() onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -46,11 +45,12 @@ class PatchSelectorCard extends StatelessWidget {
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
locator<PatcherViewModel>().selectedApp == null if (locator<PatcherViewModel>().selectedApp == null)
? I18nText('patchSelectorCard.widgetSubtitle') I18nText('patchSelectorCard.widgetSubtitle')
: locator<PatcherViewModel>().selectedPatches.isEmpty else
? I18nText('patchSelectorCard.widgetEmptySubtitle') locator<PatcherViewModel>().selectedPatches.isEmpty
: Text(_getPatchesSelection()), ? I18nText('patchSelectorCard.widgetEmptySubtitle')
: Text(_getPatchesSelection()),
], ],
), ),
); );
@@ -58,7 +58,7 @@ class PatchSelectorCard extends StatelessWidget {
String _getPatchesSelection() { String _getPatchesSelection() {
String text = ''; String text = '';
for (Patch p in locator<PatcherViewModel>().selectedPatches) { for (final Patch p in locator<PatcherViewModel>().selectedPatches) {
text += '\u2022 ${p.getSimpleName()} (v${p.version})\n'; text += '\u2022 ${p.getSimpleName()} (v${p.version})\n';
} }
return text.substring(0, text.length - 1); return text.substring(0, text.length - 1);

View File

@@ -3,11 +3,24 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class PatchItem extends StatefulWidget { class PatchItem extends StatefulWidget {
PatchItem({
Key? key,
required this.name,
required this.simpleName,
required this.description,
required this.version,
required this.packageVersion,
required this.supportedPackageVersions,
required this.isUnsupported,
required this.isSelected,
required this.onChanged,
this.child,
}) : super(key: key);
final String name; final String name;
final String simpleName; final String simpleName;
final String description; final String description;
@@ -21,20 +34,6 @@ class PatchItem extends StatefulWidget {
final toast = locator<Toast>(); final toast = locator<Toast>();
final _managerAPI = locator<ManagerAPI>(); final _managerAPI = locator<ManagerAPI>();
PatchItem(
{Key? key,
required this.name,
required this.simpleName,
required this.description,
required this.version,
required this.packageVersion,
required this.supportedPackageVersions,
required this.isUnsupported,
required this.isSelected,
required this.onChanged,
this.child})
: super(key: key);
@override @override
State<PatchItem> createState() => _PatchItemState(); State<PatchItem> createState() => _PatchItemState();
} }
@@ -75,30 +74,31 @@ class _PatchItemState extends State<PatchItem> {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[ children: <Widget>[
Text( Expanded(
widget.simpleName, child: Text(
maxLines: 2, widget.simpleName,
overflow: TextOverflow.visible, maxLines: 2,
style: const TextStyle( overflow: TextOverflow.visible,
fontSize: 16, style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 16,
), fontWeight: FontWeight.w600,
), ),
const SizedBox(width: 6),
Text(
widget.version,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.secondary,
), ),
), ),
], ],
), ),
const SizedBox(height: 1),
Text(
widget.version,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.secondary,
),
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
widget.description, widget.description,
softWrap: true, softWrap: true,
maxLines: 3,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
@@ -139,38 +139,37 @@ class _PatchItemState extends State<PatchItem> {
) )
], ],
), ),
widget.isUnsupported if (widget.isUnsupported)
? Row( Row(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: TextButton.icon( child: TextButton.icon(
label: I18nText('warning'), label: I18nText('warning'),
icon: const Icon(Icons.warning, size: 20.0), icon: const Icon(Icons.warning, size: 20.0),
onPressed: () => _showUnsupportedWarningDialog(), onPressed: () => _showUnsupportedWarningDialog(),
style: ButtonStyle( style: ButtonStyle(
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
side: BorderSide( side: BorderSide(
width: 1, color: Theme.of(context).colorScheme.secondary,
color:
Theme.of(context).colorScheme.secondary,
),
),
),
backgroundColor: MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
), ),
), ),
), ),
backgroundColor: MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
), ),
], ),
) ),
: Container(), ],
)
else
Container(),
widget.child ?? const SizedBox(), widget.child ?? const SizedBox(),
], ],
), ),

View File

@@ -3,8 +3,8 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
class OptionsTextField extends StatelessWidget { class OptionsTextField extends StatelessWidget {
final String hint;
const OptionsTextField({Key? key, required this.hint}) : super(key: key); const OptionsTextField({Key? key, required this.hint}) : super(key: key);
final String hint;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -12,7 +12,7 @@ class OptionsTextField extends StatelessWidget {
final sWidth = MediaQuery.of(context).size.width; final sWidth = MediaQuery.of(context).size.width;
return Container( return Container(
margin: const EdgeInsets.only(top: 12, bottom: 6), margin: const EdgeInsets.only(top: 12, bottom: 6),
padding: const EdgeInsets.all(0), padding: EdgeInsets.zero,
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
constraints: BoxConstraints( constraints: BoxConstraints(
@@ -28,9 +28,9 @@ class OptionsTextField extends StatelessWidget {
} }
class OptionsFilePicker extends StatelessWidget { class OptionsFilePicker extends StatelessWidget {
final String optionName;
const OptionsFilePicker({Key? key, required this.optionName}) const OptionsFilePicker({Key? key, required this.optionName})
: super(key: key); : super(key: key);
final String optionName;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -61,7 +61,7 @@ class OptionsFilePicker extends StatelessWidget {
child: Text( child: Text(
'Select File', 'Select File',
style: TextStyle( style: TextStyle(
color: Theme.of(context).textTheme.bodyText1?.color, color: Theme.of(context).textTheme.bodyLarge?.color,
), ),
), ),
), ),

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/utils/about_info.dart'; import 'package:revanced_manager/utils/about_info.dart';
import 'package:flutter/services.dart';
class AboutWidget extends StatefulWidget { class AboutWidget extends StatefulWidget {
const AboutWidget({Key? key, this.padding}) : super(key: key); const AboutWidget({Key? key, this.padding}) : super(key: key);

View File

@@ -1,14 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomSwitch extends StatelessWidget { class CustomSwitch extends StatelessWidget {
final ValueChanged<bool> onChanged;
final bool value;
const CustomSwitch({ const CustomSwitch({
Key? key, Key? key,
required this.onChanged, required this.onChanged,
required this.value, required this.value,
}) : super(key: key); }) : super(key: key);
final ValueChanged<bool> onChanged;
final bool value;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -45,7 +44,7 @@ class CustomSwitch extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
color: value color: value
? Theme.of(context).colorScheme.primaryContainer ? Theme.of(context).colorScheme.primaryContainer
: Colors.white, : Theme.of(context).colorScheme.surface,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black12.withOpacity(0.1), color: Colors.black12.withOpacity(0.1),

View File

@@ -2,12 +2,6 @@ import 'package:flutter/material.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch.dart'; import 'package:revanced_manager/ui/widgets/settingsView/custom_switch.dart';
class CustomSwitchTile extends StatelessWidget { class CustomSwitchTile extends StatelessWidget {
final Widget title;
final Widget subtitle;
final bool value;
final Function(bool) onTap;
final EdgeInsetsGeometry? padding;
const CustomSwitchTile({ const CustomSwitchTile({
Key? key, Key? key,
required this.title, required this.title,
@@ -16,6 +10,11 @@ class CustomSwitchTile extends StatelessWidget {
required this.onTap, required this.onTap,
this.padding, this.padding,
}) : super(key: key); }) : super(key: key);
final Widget title;
final Widget subtitle;
final bool value;
final Function(bool) onTap;
final EdgeInsetsGeometry? padding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -1,12 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomTextField extends StatelessWidget { class CustomTextField extends StatelessWidget {
final TextEditingController inputController;
final Widget label;
final String hint;
final Widget? leadingIcon;
final Function(String)? onChanged;
const CustomTextField({ const CustomTextField({
Key? key, Key? key,
required this.inputController, required this.inputController,
@@ -15,6 +9,11 @@ class CustomTextField extends StatelessWidget {
this.leadingIcon, this.leadingIcon,
required this.onChanged, required this.onChanged,
}) : super(key: key); }) : super(key: key);
final TextEditingController inputController;
final Widget label;
final String hint;
final Widget? leadingIcon;
final Function(String)? onChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -45,10 +44,8 @@ class CustomTextField extends StatelessWidget {
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
width: 1.0,
), ),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
gapPadding: 4.0,
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
@@ -60,14 +57,12 @@ class CustomTextField extends StatelessWidget {
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
width: 1.0,
), ),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
width: 1.0,
), ),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),

View File

@@ -2,12 +2,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_manage_api_url.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_manage_sources.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
final _settingsViewModel = SettingsViewModel(); final _settingsViewModel = SettingsViewModel();
@@ -21,6 +22,7 @@ class SAdvancedSection extends StatelessWidget {
children: <Widget>[ children: <Widget>[
SManageApiUrlUI(), SManageApiUrlUI(),
SManageSourcesUI(), SManageSourcesUI(),
// SManageKeystorePasswordUI(),
SExperimentalUniversalPatches(), SExperimentalUniversalPatches(),
SExperimentalPatches(), SExperimentalPatches(),
ListTile( ListTile(
@@ -36,7 +38,7 @@ class SAdvancedSection extends StatelessWidget {
), ),
), ),
subtitle: I18nText('settingsView.deleteKeystoreHint'), subtitle: I18nText('settingsView.deleteKeystoreHint'),
onTap: () => _settingsViewModel.deleteKeystore, onTap: () => _showDeleteKeystoreDialog(context),
), ),
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
@@ -71,4 +73,31 @@ class SAdvancedSection extends StatelessWidget {
], ],
); );
} }
Future<void> _showDeleteKeystoreDialog(context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
'settingsView.deleteKeystoreDialogText',
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.deleteKeystore()
},
)
],
),
);
}
} }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
class SExperimentalPatches extends StatefulWidget { class SExperimentalPatches extends StatefulWidget {
const SExperimentalPatches({super.key}); const SExperimentalPatches({super.key});
@@ -15,8 +14,8 @@ final _settingsViewModel = SettingsViewModel();
class _SExperimentalPatchesState extends State<SExperimentalPatches> { class _SExperimentalPatchesState extends State<SExperimentalPatches> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomSwitchTile( return SwitchListTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.experimentalPatchesLabel', 'settingsView.experimentalPatchesLabel',
child: const Text( child: const Text(
@@ -29,7 +28,7 @@ class _SExperimentalPatchesState extends State<SExperimentalPatches> {
), ),
subtitle: I18nText('settingsView.experimentalPatchesHint'), subtitle: I18nText('settingsView.experimentalPatchesHint'),
value: _settingsViewModel.areExperimentalPatchesEnabled(), value: _settingsViewModel.areExperimentalPatchesEnabled(),
onTap: (value) { onChanged: (value) {
setState(() { setState(() {
_settingsViewModel.useExperimentalPatches(value); _settingsViewModel.useExperimentalPatches(value);
}); });

View File

@@ -1,22 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
class SExperimentalUniversalPatches extends StatefulWidget { class SExperimentalUniversalPatches extends StatefulWidget {
const SExperimentalUniversalPatches({super.key}); const SExperimentalUniversalPatches({super.key});
@override @override
State<SExperimentalUniversalPatches> createState() => _SExperimentalUniversalPatchesState(); State<SExperimentalUniversalPatches> createState() =>
_SExperimentalUniversalPatchesState();
} }
final _settingsViewModel = SettingsViewModel(); final _settingsViewModel = SettingsViewModel();
class _SExperimentalUniversalPatchesState extends State<SExperimentalUniversalPatches> { class _SExperimentalUniversalPatchesState
extends State<SExperimentalUniversalPatches> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomSwitchTile( return SwitchListTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.experimentalUniversalPatchesLabel', 'settingsView.experimentalUniversalPatchesLabel',
child: const Text( child: const Text(
@@ -29,7 +30,7 @@ class _SExperimentalUniversalPatchesState extends State<SExperimentalUniversalPa
), ),
subtitle: I18nText('settingsView.experimentalUniversalPatchesHint'), subtitle: I18nText('settingsView.experimentalUniversalPatchesHint'),
value: _settingsViewModel.areUniversalPatchesEnabled(), value: _settingsViewModel.areUniversalPatchesEnabled(),
onTap: (value) { onChanged: (value) {
setState(() { setState(() {
_settingsViewModel.showUniversalPatches(value); _settingsViewModel.showUniversalPatches(value);
}); });

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
@@ -43,6 +44,40 @@ class SExportSection extends StatelessWidget {
subtitle: I18nText('settingsView.importPatchesHint'), subtitle: I18nText('settingsView.importPatchesHint'),
onTap: () => _settingsViewModel.importPatches(), onTap: () => _settingsViewModel.importPatches(),
), ),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.exportKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.exportKeystoreHint'),
onTap: () => _settingsViewModel.exportKeystore(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.importKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.importKeystoreHint'),
onTap: () {
_settingsViewModel.importKeystore();
final sManageKeystorePassword = SManageKeystorePassword();
sManageKeystorePassword.showKeystoreDialog(context);
},
),
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(

View File

@@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SLoggingSection extends StatelessWidget {
const SLoggingSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.logsSectionTitle',
children: <Widget>[
CustomSwitchTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.sentryLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.sentryHint'),
value: _settingsViewModel.isSentryEnabled(),
onTap: (value) => _settingsViewModel.useSentry(value),
),
],
);
}
}

View File

@@ -2,14 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
class SettingsSection extends StatelessWidget { class SettingsSection extends StatelessWidget {
final String title;
final List<Widget> children;
const SettingsSection({ const SettingsSection({
Key? key, Key? key,
required this.title, required this.title,
required this.children, required this.children,
}) : super(key: key); }) : super(key: key);
final String title;
final List<Widget> children;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -2,11 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
class SettingsTileDialog extends StatelessWidget { class SettingsTileDialog extends StatelessWidget {
final String title;
final String subtitle;
final Function()? onTap;
final EdgeInsetsGeometry? padding;
const SettingsTileDialog({ const SettingsTileDialog({
Key? key, Key? key,
required this.title, required this.title,
@@ -14,6 +9,10 @@ class SettingsTileDialog extends StatelessWidget {
required this.onTap, required this.onTap,
this.padding, this.padding,
}) : super(key: key); }) : super(key: key);
final String title;
final String subtitle;
final Function()? onTap;
final EdgeInsetsGeometry? padding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -1,13 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class SocialMediaItem extends StatelessWidget { class SocialMediaItem extends StatelessWidget {
final Widget? icon;
final Widget title;
final Widget? subtitle;
final String? url;
const SocialMediaItem({ const SocialMediaItem({
Key? key, Key? key,
this.icon, this.icon,
@@ -15,6 +9,10 @@ class SocialMediaItem extends StatelessWidget {
this.subtitle, this.subtitle,
this.url, this.url,
}) : super(key: key); }) : super(key: key);
final Widget? icon;
final Widget title;
final Widget? subtitle;
final String? url;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -6,12 +6,11 @@ import 'package:revanced_manager/ui/widgets/settingsView/social_media_item.dart'
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class SocialMediaWidget extends StatelessWidget { class SocialMediaWidget extends StatelessWidget {
final EdgeInsetsGeometry? padding;
const SocialMediaWidget({ const SocialMediaWidget({
Key? key, Key? key,
this.padding, this.padding,
}) : super(key: key); }) : super(key: key);
final EdgeInsetsGeometry? padding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -1,19 +1,13 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:expandable/expandable.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:timeago/timeago.dart'; import 'package:timeago/timeago.dart';
class ApplicationItem extends StatefulWidget { class ApplicationItem extends StatefulWidget {
final Uint8List icon;
final String name;
final DateTime patchDate;
final List<String> changelog;
final bool isUpdatableApp;
final Function() onPressed;
const ApplicationItem({ const ApplicationItem({
Key? key, Key? key,
required this.icon, required this.icon,
@@ -23,6 +17,12 @@ class ApplicationItem extends StatefulWidget {
required this.isUpdatableApp, required this.isUpdatableApp,
required this.onPressed, required this.onPressed,
}) : super(key: key); }) : super(key: key);
final Uint8List icon;
final String name;
final DateTime patchDate;
final List<String> changelog;
final bool isUpdatableApp;
final Function() onPressed;
@override @override
State<ApplicationItem> createState() => _ApplicationItemState(); State<ApplicationItem> createState() => _ApplicationItemState();
@@ -33,7 +33,7 @@ class _ApplicationItemState extends State<ApplicationItem>
late AnimationController _animationController; late AnimationController _animationController;
@override @override
initState() { void initState() {
super.initState(); super.initState();
_animationController = AnimationController( _animationController = AnimationController(
vsync: this, vsync: this,
@@ -49,95 +49,100 @@ class _ApplicationItemState extends State<ApplicationItem>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ExpandableController expController = ExpandableController(); final ExpandableController expController = ExpandableController();
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
child: CustomCard( child: CustomCard(
onTap: () { onTap: () {
expController.toggle(); expController.toggle();
_animationController.isCompleted _animationController.isCompleted
? _animationController.reverse() ? _animationController.reverse()
: _animationController.forward(); : _animationController.forward();
}, },
child: ExpandablePanel( child: ExpandablePanel(
controller: expController, controller: expController,
theme: const ExpandableThemeData( theme: const ExpandableThemeData(
inkWellBorderRadius: BorderRadius.all(Radius.circular(16)), inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
tapBodyToCollapse: false, tapBodyToCollapse: false,
tapBodyToExpand: false, tapBodyToExpand: false,
tapHeaderToExpand: false, tapHeaderToExpand: false,
hasIcon: false, hasIcon: false,
animationDuration: Duration(milliseconds: 450), animationDuration: Duration(milliseconds: 450),
), ),
header: Row( header: Row(
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
width: 40, width: 40,
child: Image.memory(widget.icon, height: 40, width: 40), child: Image.memory(widget.icon, height: 40, width: 40),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Padding( Padding(
padding: const EdgeInsets.only(left: 15.0), padding: const EdgeInsets.only(left: 15.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.name.length > 12
? '${widget.name.substring(0, 12)}...'
: widget.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(format(widget.patchDate)),
],
),
),
const Spacer(),
RotationTransition(
turns: Tween(begin: 0.0, end: 0.50)
.animate(_animationController),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_drop_down),
),
),
const SizedBox(width: 8),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[ children: <Widget>[
CustomMaterialButton( Text(
label: widget.isUpdatableApp widget.name.length > 12
? I18nText('applicationItem.patchButton') ? '${widget.name.substring(0, 12)}...'
: I18nText('applicationItem.infoButton'), : widget.name,
onPressed: widget.onPressed, style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
), ),
Text(format(widget.patchDate)),
], ],
), ),
], ),
), const Spacer(),
collapsed: const SizedBox(), RotationTransition(
expanded: Padding( turns:
padding: const EdgeInsets.only( Tween(begin: 0.0, end: 0.50).animate(_animationController),
top: 16.0, left: 4.0, right: 4.0, bottom: 4.0), child: const Padding(
child: Column( padding: EdgeInsets.all(8.0),
crossAxisAlignment: CrossAxisAlignment.start, child: Icon(Icons.arrow_drop_down),
),
),
const SizedBox(width: 8),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[ children: <Widget>[
I18nText( CustomMaterialButton(
'applicationItem.changelogLabel', label: widget.isUpdatableApp
child: const Text( ? I18nText('applicationItem.patchButton')
'', : I18nText('applicationItem.infoButton'),
style: TextStyle(fontWeight: FontWeight.w700), onPressed: widget.onPressed,
),
), ),
const SizedBox(height: 4),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
], ],
), ),
],
),
collapsed: const SizedBox(),
expanded: Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 4.0,
right: 4.0,
bottom: 4.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
'applicationItem.changelogLabel',
child: const Text(
'',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
const SizedBox(height: 4),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
],
), ),
), ),
)); ),
),
);
} }
} }

View File

@@ -1,21 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomCard extends StatelessWidget { class CustomCard extends StatelessWidget {
const CustomCard({
Key? key,
this.isFilled = true,
required this.child,
this.onTap,
this.padding,
this.backgroundColor,
}) : super(key: key);
final bool isFilled; final bool isFilled;
final Widget child; final Widget child;
final Function()? onTap; final Function()? onTap;
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
final Color? backgroundColor; final Color? backgroundColor;
const CustomCard(
{Key? key,
this.isFilled = true,
required this.child,
this.onTap,
this.padding,
this.backgroundColor})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(

View File

@@ -1,16 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomChip extends StatelessWidget { class CustomChip extends StatelessWidget {
final Widget label;
final bool isSelected;
final Function(bool)? onSelected;
const CustomChip({ const CustomChip({
Key? key, Key? key,
required this.label, required this.label,
this.isSelected = false, this.isSelected = false,
this.onSelected, this.onSelected,
}) : super(key: key); }) : super(key: key);
final Widget label;
final bool isSelected;
final Function(bool)? onSelected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -18,7 +17,7 @@ class CustomChip extends StatelessWidget {
showCheckmark: false, showCheckmark: false,
label: label, label: label,
selected: isSelected, selected: isSelected,
labelStyle: Theme.of(context).textTheme.subtitle2!.copyWith( labelStyle: Theme.of(context).textTheme.titleSmall!.copyWith(
color: isSelected color: isSelected
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.secondary, : Theme.of(context).colorScheme.secondary,

View File

@@ -1,11 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomMaterialButton extends StatelessWidget { class CustomMaterialButton extends StatelessWidget {
final Widget label;
final bool isFilled;
final bool isExpanded;
final Function()? onPressed;
const CustomMaterialButton({ const CustomMaterialButton({
Key? key, Key? key,
required this.label, required this.label,
@@ -13,6 +8,10 @@ class CustomMaterialButton extends StatelessWidget {
this.isExpanded = false, this.isExpanded = false,
required this.onPressed, required this.onPressed,
}) : super(key: key); }) : super(key: key);
final Widget label;
final bool isFilled;
final bool isExpanded;
final Function()? onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -28,7 +27,6 @@ class CustomMaterialButton extends StatelessWidget {
side: isFilled side: isFilled
? BorderSide.none ? BorderSide.none
: BorderSide( : BorderSide(
width: 1,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
), ),
@@ -50,12 +48,6 @@ class CustomMaterialButton extends StatelessWidget {
// ignore: must_be_immutable // ignore: must_be_immutable
class TimerButton extends StatefulWidget { class TimerButton extends StatefulWidget {
Widget label;
bool isFilled;
int seconds;
final bool isRunning;
final Function()? onTimerEnd;
TimerButton({ TimerButton({
Key? key, Key? key,
required this.seconds, required this.seconds,
@@ -64,6 +56,11 @@ class TimerButton extends StatefulWidget {
this.label = const Text(''), this.label = const Text(''),
this.isFilled = true, this.isFilled = true,
}) : super(key: key); }) : super(key: key);
Widget label;
bool isFilled;
int seconds;
final bool isRunning;
final Function()? onTimerEnd;
@override @override
State<TimerButton> createState() => _TimerButtonState(); State<TimerButton> createState() => _TimerButtonState();
@@ -101,7 +98,6 @@ class _TimerButtonState extends State<TimerButton> {
side: widget.isFilled side: widget.isFilled
? BorderSide.none ? BorderSide.none
: BorderSide( : BorderSide(
width: 1,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
), ),

View File

@@ -1,14 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomPopupMenu extends StatelessWidget { class CustomPopupMenu extends StatelessWidget {
final Function(dynamic) onSelected;
final Map<int, Widget> children;
const CustomPopupMenu({ const CustomPopupMenu({
Key? key, Key? key,
required this.onSelected, required this.onSelected,
required this.children, required this.children,
}) : super(key: key); }) : super(key: key);
final Function(dynamic) onSelected;
final Map<int, Widget> children;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -1,12 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomSliverAppBar extends StatelessWidget { class CustomSliverAppBar extends StatelessWidget {
final Widget title;
final List<Widget>? actions;
final PreferredSizeWidget? bottom;
final bool isMainView;
final Function()? onBackButtonPressed;
const CustomSliverAppBar({ const CustomSliverAppBar({
Key? key, Key? key,
required this.title, required this.title,
@@ -15,13 +9,16 @@ class CustomSliverAppBar extends StatelessWidget {
this.isMainView = false, this.isMainView = false,
this.onBackButtonPressed, this.onBackButtonPressed,
}) : super(key: key); }) : super(key: key);
final Widget title;
final List<Widget>? actions;
final PreferredSizeWidget? bottom;
final bool isMainView;
final Function()? onBackButtonPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverAppBar( return SliverAppBar(
pinned: true, pinned: true,
snap: false,
floating: false,
expandedHeight: 100.0, expandedHeight: 100.0,
automaticallyImplyLeading: !isMainView, automaticallyImplyLeading: !isMainView,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
@@ -36,7 +33,7 @@ class CustomSliverAppBar extends StatelessWidget {
: IconButton( : IconButton(
icon: Icon( icon: Icon(
Icons.arrow_back, Icons.arrow_back,
color: Theme.of(context).textTheme.headline6!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
onPressed: onPressed:
onBackButtonPressed ?? () => Navigator.of(context).pop(), onBackButtonPressed ?? () => Navigator.of(context).pop(),

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