chore: merge dev to main (#1239)

This commit is contained in:
Benjamin 2023-09-20 17:35:01 -07:00 committed by GitHub
commit c59d4aea81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 334 additions and 199 deletions

View File

@ -1,38 +0,0 @@
name: Analyze Code
on:
push:
branches: [ "dev" ]
paths:
- "**.dart"
- ".github/workflows/analyze.yml"
pull_request:
branches: [ "main", "dev" ]
types:
- opened
- reopened
- synchronize
- ready_for_review
paths:
- "**.dart"
- ".github/workflows/analyze.yml"
jobs:
build:
name: "Static analysis & format check"
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

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# Make sure the release step uses its own credentials: # Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages # https://github.com/cycjimmy/semantic-release-action#private-packages

View File

@ -9,7 +9,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 11 - name: Set up JDK 11

View File

@ -3,29 +3,33 @@
The official ReVanced Manager based on Flutter. The official ReVanced Manager based on Flutter.
## 🔽 Download ## 🔽 Download
To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
## 📝 Prerequisites ## 📝 Prerequisites
1. Android 8 or higher 1. Android 8 or higher
2. Does not work on some armv7 devices 2. Incompatible with certain ARMv7 devices
## 📃 Documentation
The documentation can be found [here](https://github.com/revanced/revanced-manager/tree/main/docs).
## 🔴 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).
## 💭 Discussion
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
## 🌐 Translation ## 🌐 Translation
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced) [![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app) We're accepting translations on [Crowdin](https://translate.revanced.app).
## 🛠️ 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](/docs/4_building.md) 3. Add your GitHub token in gradle.properties like [this](/docs/4_building.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)
7. To build release apk run `flutter build apk` 7. To build release APK run `flutter build apk`

View File

@ -25,8 +25,7 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true" android:extractNativeLibs="true">
android:enableOnBackInvokedCallback="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@ -11,6 +11,9 @@ import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.include
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.PatchResult import app.revanced.patcher.patch.PatchResult
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
@ -89,6 +92,29 @@ class MainActivity : FlutterActivity() {
stopResult = result stopResult = result
} }
"getPatches" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
if (patchBundleFilePath != null) {
val patches = PatchBundleLoader.Dex(
File(patchBundleFilePath)
).map { patch ->
val map = HashMap<String, Any>()
map["\"name\""] = "\"${patch.patchName.replace("\"","\\\"")}\""
map["\"description\""] = "\"${patch.description?.replace("\"","\\\"")}\""
map["\"excluded\""] = !patch.include
map["\"dependencies\""] = patch.dependencies?.map { "\"${it.java.patchName}\"" } ?: emptyList<Any>()
map["\"compatiblePackages\""] = patch.compatiblePackages?.map {
val map2 = HashMap<String, Any>()
map2["\"name\""] = "\"${it.name}\""
map2["\"versions\""] = it.versions.map { version -> "\"${version}\"" }
map2
} ?: emptyList<Any>()
map
}
result.success(patches)
} else result.notImplemented()
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }

View File

@ -178,8 +178,10 @@
"exportSectionTitle": "Import & export", "exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs", "logsSectionTitle": "Logs",
"darkThemeLabel": "Dark mode", "themeModeLabel": "App theme",
"darkThemeHint": "Welcome to the dark side", "systemThemeLabel": "System",
"lightThemeLabel": "Light",
"darkThemeLabel": "Dark",
"dynamicThemeLabel": "Material You", "dynamicThemeLabel": "Material You",
"dynamicThemeHint": "Enjoy an experience closer to your device", "dynamicThemeHint": "Enjoy an experience closer to your device",

View File

@ -222,10 +222,8 @@ class GithubAPI {
final String downloadUrl = asset['browser_download_url']; final String downloadUrl = asset['browser_download_url'];
if (extension == '.apk') { if (extension == '.apk') {
_managerAPI.setIntegrationsDownloadURL(downloadUrl); _managerAPI.setIntegrationsDownloadURL(downloadUrl);
} else if (extension == '.json') {
_managerAPI.setPatchesDownloadURL(downloadUrl, false);
} else { } else {
_managerAPI.setPatchesDownloadURL(downloadUrl, true); _managerAPI.setPatchesDownloadURL(downloadUrl);
} }
return await DefaultCacheManager().getSingleFile( return await DefaultCacheManager().getSingleFile(
downloadUrl, downloadUrl,

View File

@ -11,6 +11,7 @@ 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';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_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/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
@ -26,6 +27,7 @@ class ManagerAPI {
final String patcherRepo = 'revanced-patcher'; final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli'; final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs; late SharedPreferences _prefs;
List<Patch> patches = [];
bool isRooted = false; bool isRooted = false;
String storedPatchesFile = '/selected-patches.json'; String storedPatchesFile = '/selected-patches.json';
String keystoreFile = String keystoreFile =
@ -41,11 +43,11 @@ class ManagerAPI {
String? patchesVersion = ''; String? patchesVersion = '';
String? integrationsVersion = ''; String? integrationsVersion = '';
bool isDefaultPatchesRepo() { bool isDefaultPatchesRepo() {
return getPatchesRepo() == 'revanced/revanced-patches'; return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
} }
bool isDefaultIntegrationsRepo() { bool isDefaultIntegrationsRepo() {
return getIntegrationsRepo() == 'revanced/revanced-integrations'; return getIntegrationsRepo().toLowerCase() == 'revanced/revanced-integrations';
} }
Future<void> initialize() async { Future<void> initialize() async {
@ -79,12 +81,12 @@ class ManagerAPI {
await _prefs.setString('repoUrl', url); await _prefs.setString('repoUrl', url);
} }
String getPatchesDownloadURL(bool bundle) { String getPatchesDownloadURL() {
return _prefs.getString('patchesDownloadURL-$bundle') ?? ''; return _prefs.getString('patchesDownloadURL') ?? '';
} }
Future<void> setPatchesDownloadURL(String value, bool bundle) async { Future<void> setPatchesDownloadURL(String value) async {
await _prefs.setString('patchesDownloadURL-$bundle', value); await _prefs.setString('patchesDownloadURL', value);
} }
String getPatchesRepo() { String getPatchesRepo() {
@ -197,12 +199,12 @@ class ManagerAPI {
await _prefs.setBool('useDynamicTheme', value); await _prefs.setBool('useDynamicTheme', value);
} }
bool getUseDarkTheme() { int getThemeMode() {
return _prefs.getBool('useDarkTheme') ?? false; return _prefs.getInt('themeMode') ?? 2;
} }
Future<void> setUseDarkTheme(bool value) async { Future<void> setThemeMode(int value) async {
await _prefs.setBool('useDarkTheme', value); await _prefs.setInt('themeMode', value);
} }
bool areUniversalPatchesEnabled() { bool areUniversalPatchesEnabled() {
@ -300,28 +302,38 @@ class ManagerAPI {
} }
Future<List<Patch>> getPatches() async { Future<List<Patch>> getPatches() async {
try { if (patches.isNotEmpty) {
final String repoName = getPatchesRepo(); return patches;
final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL(false);
return await _githubAPI.getPatches(
repoName,
currentVersion,
url,
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return [];
} }
final File? patchBundleFile = await downloadPatches();
if (patchBundleFile != null) {
try {
final patchesObject = await PatcherAPI.patcherChannel.invokeMethod(
'getPatches',
{
'patchBundleFilePath': patchBundleFile.path,
},
);
final List<Map<String, dynamic>> patchesMap = [];
patchesObject.forEach((patch) {
patchesMap.add(jsonDecode('$patch'));
});
patches = patchesMap.map((patch) => Patch.fromJson(patch)).toList();
return patches;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
return List.empty();
} }
Future<File?> downloadPatches() async { Future<File?> downloadPatches() async {
try { try {
final String repoName = getPatchesRepo(); final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion(); final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL(true); final String url = getPatchesDownloadURL();
return await _githubAPI.getPatchesReleaseFile( return await _githubAPI.getPatchesReleaseFile(
'.jar', '.jar',
repoName, repoName,
@ -447,8 +459,7 @@ class ManagerAPI {
Future<void> setCurrentPatchesVersion(String version) async { Future<void> setCurrentPatchesVersion(String version) async {
await _prefs.setString('patchesVersion', version); await _prefs.setString('patchesVersion', version);
await setPatchesDownloadURL('', false); await setPatchesDownloadURL('');
await setPatchesDownloadURL('', true);
await downloadPatches(); await downloadPatches();
} }

View File

@ -25,12 +25,13 @@ class PatcherAPI {
late Directory _tmpDir; late Directory _tmpDir;
late File _keyStoreFile; late File _keyStoreFile;
List<Patch> _patches = []; List<Patch> _patches = [];
List<Patch> _universalPatches = [];
List<String> _compatiblePackages = [];
Map filteredPatches = <String, List<Patch>>{}; Map filteredPatches = <String, List<Patch>>{};
File? _outFile; File? _outFile;
Future<void> initialize() async { Future<void> initialize() async {
await _loadPatches(); await _loadPatches();
await _managerAPI.downloadPatches();
await _managerAPI.downloadIntegrations(); await _managerAPI.downloadIntegrations();
final Directory appCache = await getTemporaryDirectory(); final Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache; _dataDir = await getExternalStorageDirectory() ?? appCache;
@ -45,6 +46,24 @@ class PatcherAPI {
} }
} }
List<String> getCompatiblePackages() {
final List<String> compatiblePackages = [];
for (final Patch patch in _patches) {
for (final Package package in patch.compatiblePackages) {
if (!compatiblePackages.contains(package.name)) {
compatiblePackages.add(package.name);
}
}
}
return compatiblePackages;
}
List<Patch> getUniversalPatches() {
return _patches
.where((patch) => patch.compatiblePackages.isEmpty)
.toList();
}
Future<void> _loadPatches() async { Future<void> _loadPatches() async {
try { try {
if (_patches.isEmpty) { if (_patches.isEmpty) {
@ -56,6 +75,9 @@ class PatcherAPI {
} }
_patches = List.empty(); _patches = List.empty();
} }
_compatiblePackages = getCompatiblePackages();
_universalPatches = getUniversalPatches();
} }
Future<List<ApplicationWithIcon>> getFilteredInstalledApps( Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
@ -63,48 +85,43 @@ class PatcherAPI {
) async { ) async {
final List<ApplicationWithIcon> filteredApps = []; final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded = final bool allAppsIncluded =
_patches.any((patch) => patch.compatiblePackages.isEmpty) && _universalPatches.isNotEmpty &&
showUniversalPatches; showUniversalPatches;
if (allAppsIncluded) { if (allAppsIncluded) {
final allPackages = await DeviceApps.getInstalledApplications( final appList = await DeviceApps.getInstalledApplications(
includeAppIcons: true, includeAppIcons: true,
onlyAppsWithLaunchIntent: true, onlyAppsWithLaunchIntent: true,
); );
for (final pkg in allPackages) {
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) { for(final app in appList) {
final appInfo = await DeviceApps.getApp( filteredApps.add(app as ApplicationWithIcon);
pkg.packageName,
true,
) as ApplicationWithIcon?;
if (appInfo != null) {
filteredApps.add(appInfo);
}
}
} }
} }
for (final Patch patch in _patches) { for (final packageName in _compatiblePackages) {
for (final Package package in patch.compatiblePackages) { try {
try { if (!filteredApps.any((app) => app.packageName == packageName)) {
if (!filteredApps.any((app) => app.packageName == package.name)) { final ApplicationWithIcon? app = await DeviceApps.getApp(
final ApplicationWithIcon? app = await DeviceApps.getApp( packageName,
package.name, true,
true, ) as ApplicationWithIcon?;
) as ApplicationWithIcon?; if (app != null) {
if (app != null) { filteredApps.add(app);
filteredApps.add(app);
}
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} }
} }
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
} }
} }
return filteredApps; return filteredApps;
} }
List<Patch> getFilteredPatches(String packageName) { List<Patch> getFilteredPatches(String packageName) {
if (!_compatiblePackages.contains(packageName)) {
return _universalPatches;
}
final List<Patch> patches = _patches final List<Patch> patches = _patches
.where( .where(
(patch) => (patch) =>

View File

@ -1,12 +1,16 @@
import 'dart:ui';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:dynamic_themes/dynamic_themes.dart'; import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.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/services/manager_api.dart';
import 'package:revanced_manager/theme.dart'; 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 StatefulWidget {
const DynamicThemeBuilder({ const DynamicThemeBuilder({
Key? key, Key? key,
required this.title, required this.title,
@ -17,6 +21,35 @@ class DynamicThemeBuilder extends StatelessWidget {
final Widget home; final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates; final Iterable<LocalizationsDelegate> localizationsDelegates;
@override
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
}
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> with WidgetsBindingObserver {
Brightness brightness = PlatformDispatcher.instance.platformBrightness;
final ManagerAPI _managerAPI = locator<ManagerAPI>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangePlatformBrightness() {
setState(() {
brightness = PlatformDispatcher.instance.platformBrightness;
});
if (_managerAPI.getThemeMode() < 2) {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
brightness == Brightness.light ? Brightness.dark : Brightness.light,
),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DynamicColorBuilder( return DynamicColorBuilder(
@ -50,24 +83,32 @@ class DynamicThemeBuilder extends StatelessWidget {
return DynamicTheme( return DynamicTheme(
themeCollection: ThemeCollection( themeCollection: ThemeCollection(
themes: { themes: {
0: lightCustomTheme, 0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
1: darkCustomTheme, 1: brightness == Brightness.light ? lightDynamicTheme : darkDynamicTheme,
2: lightDynamicTheme, 2: lightCustomTheme,
3: darkDynamicTheme, 3: lightDynamicTheme,
4: darkCustomTheme,
5: darkDynamicTheme,
}, },
fallbackTheme: lightCustomTheme, fallbackTheme: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
), ),
builder: (context, theme) => MaterialApp( builder: (context, theme) => MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: title, title: widget.title,
navigatorKey: StackedService.navigatorKey, navigatorKey: StackedService.navigatorKey,
onGenerateRoute: StackedRouter().onGenerateRoute, onGenerateRoute: StackedRouter().onGenerateRoute,
theme: theme, theme: theme,
home: home, home: widget.home,
localizationsDelegates: localizationsDelegates, localizationsDelegates: widget.localizationsDelegates,
), ),
); );
}, },
); );
} }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
} }

View File

@ -182,52 +182,54 @@ class InstallerViewModel extends BaseViewModel {
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.file_download_outlined), icon: const Icon(Icons.file_download_outlined),
contentPadding: const EdgeInsets.symmetric(vertical: 16), contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: ValueListenableBuilder( content: SingleChildScrollView(
valueListenable: installType, child: ValueListenableBuilder(
builder: (context, value, child) { valueListenable: installType,
return Column( builder: (context, value, child) {
mainAxisSize: MainAxisSize.min, return Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Padding( children: [
padding: const EdgeInsets.symmetric( Padding(
horizontal: 20, padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 20,
), vertical: 10,
child: I18nText( ),
'installerView.installTypeDescription', child: I18nText(
child: Text( 'installerView.installTypeDescription',
'', child: Text(
style: TextStyle( '',
fontSize: 16, style: TextStyle(
fontWeight: FontWeight.w500, fontSize: 16,
color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
), ),
), ),
), ),
), RadioListTile(
RadioListTile( title: I18nText('installerView.installNonRootType'),
title: I18nText('installerView.installNonRootType'), subtitle: I18nText('installerView.installRecommendedType'),
subtitle: I18nText('installerView.installRecommendedType'), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
contentPadding: const EdgeInsets.symmetric(horizontal: 10), value: 0,
value: 0, groupValue: value,
groupValue: value, onChanged: (selected) {
onChanged: (selected) { installType.value = selected!;
installType.value = selected!; },
}, ),
), RadioListTile(
RadioListTile( title: I18nText('installerView.installRootType'),
title: I18nText('installerView.installRootType'), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
contentPadding: const EdgeInsets.symmetric(horizontal: 10), value: 1,
value: 1, groupValue: value,
groupValue: value, onChanged: (selected) {
onChanged: (selected) { installType.value = selected!;
installType.value = selected!; },
}, ),
), ],
], );
); },
}, ),
), ),
actions: [ actions: [
CustomMaterialButton( CustomMaterialButton(

View File

@ -30,11 +30,8 @@ class NavigationViewModel extends IndexTrackingViewModel {
); );
} }
if (prefs.getBool('useDarkTheme') == null) { if (prefs.getInt('themeMode') == null) {
final bool isDark = await prefs.setInt('themeMode', 0);
MediaQuery.platformBrightnessOf(context) != Brightness.light;
await prefs.setBool('useDarkTheme', isDark);
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
} }
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(

View File

@ -8,6 +8,7 @@ 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/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
final _settingViewModel = SettingsViewModel(); final _settingViewModel = SettingsViewModel();
@ -24,37 +25,114 @@ class SUpdateTheme extends BaseViewModel {
Future<void> setUseDynamicTheme(BuildContext context, bool value) async { Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value); await _managerAPI.setUseDynamicTheme(value);
final int currentTheme = DynamicTheme.of(context)!.themeId; final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
if (currentTheme.isEven) { await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
notifyListeners(); notifyListeners();
} }
bool getDarkThemeStatus() { int getThemeMode() {
return _managerAPI.getUseDarkTheme(); return _managerAPI.getThemeMode();
} }
Future<void> setUseDarkTheme(BuildContext context, bool value) async { Future<void> setThemeMode(BuildContext context, int value) async {
await _managerAPI.setUseDarkTheme(value); await _managerAPI.setThemeMode(value);
final int currentTheme = DynamicTheme.of(context)!.themeId; final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
if (currentTheme < 2) { await DynamicTheme.of(context)!.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0); final bool isLight = value != 2 && (value == 1 || DynamicTheme.of(context)!.theme.brightness == Brightness.light);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
}
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle( SystemUiOverlayStyle(
systemNavigationBarIconBrightness: systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark, isLight ? Brightness.dark : Brightness.light,
), ),
); );
notifyListeners(); notifyListeners();
} }
I18nText getThemeModeName() {
switch (getThemeMode()) {
case 0:
return I18nText('settingsView.systemThemeLabel');
case 1:
return I18nText('settingsView.lightThemeLabel');
case 2:
return I18nText('settingsView.darkThemeLabel');
default:
return I18nText('settingsView.systemThemeLabel');
}
}
Future<void> showThemeDialog(BuildContext context) async {
final ValueNotifier<int> newTheme = ValueNotifier(getThemeMode());
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.themeModeLabel'),
icon: const Icon(Icons.palette),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: ValueListenableBuilder(
valueListenable: newTheme,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
RadioListTile(
title: I18nText('settingsView.systemThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 0,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
RadioListTile(
title: I18nText('settingsView.lightThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 1,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
RadioListTile(
title: I18nText('settingsView.darkThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 2,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
],
);
},
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
setThemeMode(context, newTheme.value);
Navigator.of(context).pop();
},
),
],
),
);
}
} }
final sUpdateTheme = SUpdateTheme();
class SUpdateThemeUI extends StatelessWidget { class SUpdateThemeUI extends StatelessWidget {
const SUpdateThemeUI({super.key}); const SUpdateThemeUI({super.key});
@ -63,10 +141,10 @@ class SUpdateThemeUI extends StatelessWidget {
return SettingsSection( return SettingsSection(
title: 'settingsView.appearanceSectionTitle', title: 'settingsView.appearanceSectionTitle',
children: <Widget>[ children: <Widget>[
SwitchListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.darkThemeLabel', 'settingsView.themeModeLabel',
child: const Text( child: const Text(
'', '',
style: TextStyle( style: TextStyle(
@ -75,11 +153,9 @@ class SUpdateThemeUI extends StatelessWidget {
), ),
), ),
), ),
subtitle: I18nText('settingsView.darkThemeHint'), trailing: CustomMaterialButton(
value: SUpdateTheme().getDarkThemeStatus(), label: sUpdateTheme.getThemeModeName(),
onChanged: (value) => SUpdateTheme().setUseDarkTheme( onPressed: () => { sUpdateTheme.showThemeDialog(context) },
context,
value,
), ),
), ),
FutureBuilder<int>( FutureBuilder<int>(

View File

@ -75,8 +75,8 @@ class SocialMediaWidget extends StatelessWidget {
SocialMediaItem( SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.youtube), icon: FaIcon(FontAwesomeIcons.youtube),
title: Text('YouTube'), title: Text('YouTube'),
subtitle: Text('youtube.com/revanced'), subtitle: Text('youtube.com/@revanced'),
url: 'https://youtube.com/revanced', url: 'https://youtube.com/@revanced',
), ),
], ],
), ),