Theme configuration

This commit is contained in:
Juan Gilsanz Polo
2024-02-23 01:26:22 +01:00
parent 57af5e926e
commit e21546693a
32 changed files with 1053 additions and 45 deletions

View File

@@ -1,3 +0,0 @@
class SharedPreferencesKeys {
static const serversInstances = "serversInstances";
}

View File

@@ -1,3 +1,5 @@
enum LoadStatus { loading, loaded, error }
enum ConnectionMethod { http, https }
enum SelectedTheme { system, light, dark }

View File

@@ -0,0 +1,6 @@
class SharedPreferencesKeys {
static const serversInstances = "serversInstances";
static const selectedTheme = "selectedTheme";
static const useDynamicTheme = "useDynamicTheme";
static const selectedColor = "selectedColor";
}

View File

@@ -0,0 +1,3 @@
class Strings {
static const createdBy = "JGeek00";
}

View File

@@ -1,4 +1,5 @@
class Urls {
static const linkdingInstallationInstructions =
"https://github.com/sissbruecker/linkding?tab=readme-ov-file#installation";
static const gitHubRepo = "https://github.com/JGeek00/linkdy";
}

View File

@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 68 (34 per locale)
/// Strings: 122 (61 per locale)
///
/// Built on 2024-02-22 at 21:21 UTC
/// Built on 2024-02-23 at 00:06 UTC
// coverage:ignore-file
// ignore_for_file: type=lint
@@ -153,6 +153,7 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
late final _StringsSearchEn search = _StringsSearchEn._(_root);
late final _StringsSettingsEn settings = _StringsSettingsEn._(_root);
late final _StringsWebviewEn webview = _StringsWebviewEn._(_root);
late final _StringsColorsEn colors = _StringsColorsEn._(_root);
}
// Path: onboarding
@@ -214,7 +215,17 @@ class _StringsSettingsEn {
// Translations
String get settings => 'Settings';
String get appSettings => 'App settings';
String get disconnectFromServer => 'Disconnect from server';
String get aboutApp => 'About the app';
String get appVersion => 'App version';
String get createdBy => 'Created by';
String get visitGooglePlay => 'Visit the application page at Google Play';
String get visitGitHubRepo => 'Visit the application repository at GitHub';
String get customizationDescription => 'Set the app theme and color palette.';
String get generalSettings => 'General settings';
String get generalSettingsDescription => 'General tweaks for the application.';
late final _StringsSettingsCustomizationEn customization = _StringsSettingsCustomizationEn._(_root);
}
// Path: webview
@@ -233,6 +244,26 @@ class _StringsWebviewEn {
String get share => 'Share';
}
// Path: colors
class _StringsColorsEn {
_StringsColorsEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get red => 'Red';
String get green => 'Green';
String get blue => 'Blue';
String get yellow => 'Yellow';
String get orange => 'Orange';
String get brown => 'Brown';
String get cyan => 'Cyan';
String get purple => 'Purple';
String get pink => 'Pink';
String get deepOrange => 'Deep orange';
String get indigo => 'Indigo';
}
// Path: links.dates
class _StringsLinksDatesEn {
_StringsLinksDatesEn._(this._root);
@@ -244,6 +275,22 @@ class _StringsLinksDatesEn {
String yesterdayAt({required Object time}) => 'Yesterday, ${time}';
}
// Path: settings.customization
class _StringsSettingsCustomizationEn {
_StringsSettingsCustomizationEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get customization => 'Customization';
String get theme => 'Theme';
String get light => 'Light';
String get dark => 'Dark';
String get systemDefined => 'System defined';
String get color => 'Color';
String get useDynamicTheme => 'Use dynamic theme';
}
// Path: <root>
class _StringsEs implements Translations {
/// You can call this constructor and build your own translation instance of this locale.
@@ -273,6 +320,7 @@ class _StringsEs implements Translations {
@override late final _StringsSearchEs search = _StringsSearchEs._(_root);
@override late final _StringsSettingsEs settings = _StringsSettingsEs._(_root);
@override late final _StringsWebviewEs webview = _StringsWebviewEs._(_root);
@override late final _StringsColorsEs colors = _StringsColorsEs._(_root);
}
// Path: onboarding
@@ -334,7 +382,17 @@ class _StringsSettingsEs implements _StringsSettingsEn {
// Translations
@override String get settings => 'Ajustes';
@override String get appSettings => 'Ajustes de la aplicación';
@override String get disconnectFromServer => 'Desconectar del servidor';
@override String get aboutApp => 'Sobre la aplicación';
@override String get appVersion => 'Versión de la aplicación';
@override String get createdBy => 'Creado por';
@override String get visitGooglePlay => 'Visita la página de la aplicación en Google Play';
@override String get visitGitHubRepo => 'Visita el repositorio de la aplicación en GitHub';
@override String get customizationDescription => 'Configura el tema y la paleta de colores.';
@override String get generalSettings => 'Ajustes generales';
@override String get generalSettingsDescription => 'Ajustes generales para la aplicación.';
@override late final _StringsSettingsCustomizationEs customization = _StringsSettingsCustomizationEs._(_root);
}
// Path: webview
@@ -353,6 +411,26 @@ class _StringsWebviewEs implements _StringsWebviewEn {
@override String get share => 'Compartir';
}
// Path: colors
class _StringsColorsEs implements _StringsColorsEn {
_StringsColorsEs._(this._root);
@override final _StringsEs _root; // ignore: unused_field
// Translations
@override String get red => 'Rojo';
@override String get green => 'Verde';
@override String get blue => 'Azul';
@override String get yellow => 'Amarillo';
@override String get orange => 'Naranja';
@override String get brown => 'Marron';
@override String get cyan => 'Cian';
@override String get purple => 'Morado';
@override String get pink => 'Rosa';
@override String get deepOrange => 'Naranja oscuro';
@override String get indigo => 'Índigo';
}
// Path: links.dates
class _StringsLinksDatesEs implements _StringsLinksDatesEn {
_StringsLinksDatesEs._(this._root);
@@ -364,6 +442,22 @@ class _StringsLinksDatesEs implements _StringsLinksDatesEn {
@override String yesterdayAt({required Object time}) => 'Ayer, ${time}';
}
// Path: settings.customization
class _StringsSettingsCustomizationEs implements _StringsSettingsCustomizationEn {
_StringsSettingsCustomizationEs._(this._root);
@override final _StringsEs _root; // ignore: unused_field
// Translations
@override String get customization => 'Personalización';
@override String get theme => 'Tema';
@override String get light => 'Claro';
@override String get dark => 'Oscuro';
@override String get systemDefined => 'Definido por el sistema';
@override String get color => 'Color';
@override String get useDynamicTheme => 'Usar tema dinámico';
}
/// Flat map(s) containing all translations.
/// Only for edge cases! For simple maps, use the map function of this library.
@@ -396,7 +490,23 @@ extension on Translations {
case 'links.dates.yesterdayAt': return ({required Object time}) => 'Yesterday, ${time}';
case 'search.search': return 'Search';
case 'settings.settings': return 'Settings';
case 'settings.appSettings': return 'App settings';
case 'settings.disconnectFromServer': return 'Disconnect from server';
case 'settings.aboutApp': return 'About the app';
case 'settings.appVersion': return 'App version';
case 'settings.createdBy': return 'Created by';
case 'settings.visitGooglePlay': return 'Visit the application page at Google Play';
case 'settings.visitGitHubRepo': return 'Visit the application repository at GitHub';
case 'settings.customizationDescription': return 'Set the app theme and color palette.';
case 'settings.generalSettings': return 'General settings';
case 'settings.generalSettingsDescription': return 'General tweaks for the application.';
case 'settings.customization.customization': return 'Customization';
case 'settings.customization.theme': return 'Theme';
case 'settings.customization.light': return 'Light';
case 'settings.customization.dark': return 'Dark';
case 'settings.customization.systemDefined': return 'System defined';
case 'settings.customization.color': return 'Color';
case 'settings.customization.useDynamicTheme': return 'Use dynamic theme';
case 'webview.goBack': return 'Go back';
case 'webview.goForward': return 'Go forward';
case 'webview.reload': return 'Reload';
@@ -404,6 +514,17 @@ extension on Translations {
case 'webview.copyLinkClipboard': return 'Copy link to clipboard';
case 'webview.linkCopiedClipboard': return 'Link copied to the clipboard.';
case 'webview.share': return 'Share';
case 'colors.red': return 'Red';
case 'colors.green': return 'Green';
case 'colors.blue': return 'Blue';
case 'colors.yellow': return 'Yellow';
case 'colors.orange': return 'Orange';
case 'colors.brown': return 'Brown';
case 'colors.cyan': return 'Cyan';
case 'colors.purple': return 'Purple';
case 'colors.pink': return 'Pink';
case 'colors.deepOrange': return 'Deep orange';
case 'colors.indigo': return 'Indigo';
default: return null;
}
}
@@ -438,7 +559,23 @@ extension on _StringsEs {
case 'links.dates.yesterdayAt': return ({required Object time}) => 'Ayer, ${time}';
case 'search.search': return 'Buscar';
case 'settings.settings': return 'Ajustes';
case 'settings.appSettings': return 'Ajustes de la aplicación';
case 'settings.disconnectFromServer': return 'Desconectar del servidor';
case 'settings.aboutApp': return 'Sobre la aplicación';
case 'settings.appVersion': return 'Versión de la aplicación';
case 'settings.createdBy': return 'Creado por';
case 'settings.visitGooglePlay': return 'Visita la página de la aplicación en Google Play';
case 'settings.visitGitHubRepo': return 'Visita el repositorio de la aplicación en GitHub';
case 'settings.customizationDescription': return 'Configura el tema y la paleta de colores.';
case 'settings.generalSettings': return 'Ajustes generales';
case 'settings.generalSettingsDescription': return 'Ajustes generales para la aplicación.';
case 'settings.customization.customization': return 'Personalización';
case 'settings.customization.theme': return 'Tema';
case 'settings.customization.light': return 'Claro';
case 'settings.customization.dark': return 'Oscuro';
case 'settings.customization.systemDefined': return 'Definido por el sistema';
case 'settings.customization.color': return 'Color';
case 'settings.customization.useDynamicTheme': return 'Usar tema dinámico';
case 'webview.goBack': return 'Ir atrás';
case 'webview.goForward': return 'Ir adelante';
case 'webview.reload': return 'Recargar';
@@ -446,6 +583,17 @@ extension on _StringsEs {
case 'webview.copyLinkClipboard': return 'Copiar enlace al portapapeles';
case 'webview.linkCopiedClipboard': return 'Enlace copiado al portapapeles.';
case 'webview.share': return 'Compartir';
case 'colors.red': return 'Rojo';
case 'colors.green': return 'Verde';
case 'colors.blue': return 'Azul';
case 'colors.yellow': return 'Amarillo';
case 'colors.orange': return 'Naranja';
case 'colors.brown': return 'Marron';
case 'colors.cyan': return 'Cian';
case 'colors.purple': return 'Morado';
case 'colors.pink': return 'Rosa';
case 'colors.deepOrange': return 'Naranja oscuro';
case 'colors.indigo': return 'Índigo';
default: return null;
}
}

View File

@@ -34,7 +34,25 @@
},
"settings": {
"settings": "Settings",
"disconnectFromServer": "Disconnect from server"
"appSettings": "App settings",
"disconnectFromServer": "Disconnect from server",
"aboutApp": "About the app",
"appVersion": "App version",
"createdBy": "Created by",
"visitGooglePlay": "Visit the application page at Google Play",
"visitGitHubRepo": "Visit the application repository at GitHub",
"customizationDescription": "Set the app theme and color palette.",
"generalSettings": "General settings",
"generalSettingsDescription": "General tweaks for the application.",
"customization": {
"customization": "Customization",
"theme": "Theme",
"light": "Light",
"dark": "Dark",
"systemDefined": "System defined",
"color": "Color",
"useDynamicTheme": "Use dynamic theme"
}
},
"webview": {
"goBack": "Go back",
@@ -44,5 +62,18 @@
"copyLinkClipboard": "Copy link to clipboard",
"linkCopiedClipboard": "Link copied to the clipboard.",
"share": "Share"
},
"colors": {
"red": "Red",
"green": "Green",
"blue": "Blue",
"yellow": "Yellow",
"orange": "Orange",
"brown": "Brown",
"cyan": "Cyan",
"purple": "Purple",
"pink": "Pink",
"deepOrange": "Deep orange",
"indigo": "Indigo"
}
}

View File

@@ -34,7 +34,25 @@
},
"settings": {
"settings": "Ajustes",
"disconnectFromServer": "Desconectar del servidor"
"appSettings": "Ajustes de la aplicación",
"disconnectFromServer": "Desconectar del servidor",
"aboutApp": "Sobre la aplicación",
"appVersion": "Versión de la aplicación",
"createdBy": "Creado por",
"visitGooglePlay": "Visita la página de la aplicación en Google Play",
"visitGitHubRepo": "Visita el repositorio de la aplicación en GitHub",
"customizationDescription": "Configura el tema y la paleta de colores.",
"generalSettings": "Ajustes generales",
"generalSettingsDescription": "Ajustes generales para la aplicación.",
"customization": {
"customization": "Personalización",
"theme": "Tema",
"light": "Claro",
"dark": "Oscuro",
"systemDefined": "Definido por el sistema",
"color": "Color",
"useDynamicTheme": "Usar tema dinámico"
}
},
"webview": {
"goBack": "Ir atrás",
@@ -44,5 +62,18 @@
"copyLinkClipboard": "Copiar enlace al portapapeles",
"linkCopiedClipboard": "Enlace copiado al portapapeles.",
"share": "Compartir"
},
"colors": {
"red": "Rojo",
"green": "Verde",
"blue": "Azul",
"yellow": "Amarillo",
"orange": "Naranja",
"brown": "Marron",
"cyan": "Cian",
"purple": "Morado",
"pink": "Rosa",
"deepOrange": "Naranja oscuro",
"indigo": "Índigo"
}
}

View File

@@ -6,8 +6,11 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:linkdy/constants/global_keys.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:linkdy/providers/app_status_provider.dart';
import 'package:linkdy/providers/app_info_provider.dart';
import 'package:linkdy/config/theme.dart';
import 'package:linkdy/constants/colors.dart';
import 'package:linkdy/i18n/strings.g.dart';
@@ -25,11 +28,14 @@ void main() async {
final sharedPreferences = await SharedPreferences.getInstance();
final appInfo = await PackageInfo.fromPlatform();
runApp(
TranslationProvider(
child: ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
appInfoProvider.overrideWithValue(appInfo),
],
child: const MyApp(),
),
@@ -42,18 +48,33 @@ class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedColor = ref.watch(appStatusProvider).selectedColor;
final useDynamicColor = ref.watch(appStatusProvider).useDynamicTheme;
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => MaterialApp.router(
title: 'Linkdy',
theme: lightDynamic != null ? lightTheme(lightDynamic) : lightThemeOldVersions(colors[0]),
darkTheme: darkDynamic != null ? darkTheme(darkDynamic) : darkThemeOldVersions(colors[0]),
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: scaffoldMessengerGlobalKey,
routerConfig: ref.watch(routerProvider),
),
builder: (lightDynamic, darkDynamic) {
ref.read(appStatusProvider.notifier).setSupportsDynamicTheme(lightDynamic != null && darkDynamic != null);
return MaterialApp.router(
title: 'Linkdy',
theme: lightDynamic != null
? useDynamicColor == true
? lightTheme(lightDynamic)
: lightThemeOldVersions(colors[selectedColor])
: lightThemeOldVersions(colors[selectedColor]),
darkTheme: darkDynamic != null
? useDynamicColor == true
? darkTheme(darkDynamic)
: darkThemeOldVersions(colors[selectedColor])
: darkThemeOldVersions(colors[selectedColor]),
themeMode: ref.watch(selectedThemeProvider),
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: scaffoldMessengerGlobalKey,
routerConfig: ref.watch(routerProvider),
);
},
);
}
}

View File

@@ -1,12 +1,15 @@
import 'package:linkdy/models/server_instance.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:linkdy/constants/enums.dart';
class AppStatus {
AppStatus();
class AppStatusModel {
SelectedTheme selectedTheme;
bool supportsDynamicTheme;
bool useDynamicTheme;
int selectedColor;
AppStatus copyWith({
SharedPreferences? sharedPreferences,
ServerInstance? serverInstance,
}) =>
AppStatus();
AppStatusModel({
this.selectedTheme = SelectedTheme.system,
this.supportsDynamicTheme = false,
this.useDynamicTheme = true,
this.selectedColor = 0,
});
}

View File

@@ -0,0 +1,9 @@
import 'package:package_info_plus/package_info_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_info_provider.g.dart';
@Riverpod(keepAlive: true)
PackageInfo appInfo(AppInfoRef ref) {
throw UnimplementedError();
}

View File

@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_info_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$appInfoHash() => r'c1b99fe8e0ba1b1ba04aa2c1ae066a3de9f1dac1';
/// See also [appInfo].
@ProviderFor(appInfo)
final appInfoProvider = Provider<PackageInfo>.internal(
appInfo,
name: r'appInfoProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$appInfoHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AppInfoRef = ProviderRef<PackageInfo>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -1,18 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:linkdy/constants/shared_preferences_keys.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:linkdy/providers/shared_preferences_provider.dart';
import 'package:linkdy/constants/enums.dart';
import 'package:linkdy/models/app_status.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'app_status_provider.g.dart';
@Riverpod(keepAlive: true)
class AppStatusProvider extends _$AppStatusProvider {
class AppStatus extends _$AppStatus {
@override
AppStatus build() {
return AppStatus();
AppStatusModel build() {
final sharedPreferences = ref.watch(sharedPreferencesProvider);
return AppStatusModel(
selectedColor: sharedPreferences.getInt(SharedPreferencesKeys.selectedColor) ?? 0,
selectedTheme: SelectedTheme.values[sharedPreferences.getInt(SharedPreferencesKeys.selectedTheme) ?? 0],
useDynamicTheme: sharedPreferences.getBool(SharedPreferencesKeys.useDynamicTheme) ?? true,
);
}
void setSharedPreferencesInstance(SharedPreferences instance) {
state = state.copyWith(sharedPreferences: instance);
void setTheme(SelectedTheme theme) {
state.selectedTheme = theme;
ref.read(sharedPreferencesProvider).setInt(SharedPreferencesKeys.selectedTheme, theme.index);
ref.notifyListeners();
}
void setSupportsDynamicTheme(bool value) {
state.supportsDynamicTheme = value;
}
void setUseDynamicTheme(bool value) {
state.useDynamicTheme = value;
ref.read(sharedPreferencesProvider).setBool(SharedPreferencesKeys.useDynamicTheme, value);
ref.notifyListeners();
}
void setSelectedColor(int value) {
state.selectedColor = value;
ref.read(sharedPreferencesProvider).setInt(SharedPreferencesKeys.selectedColor, value);
ref.notifyListeners();
}
}
@riverpod
ThemeMode selectedTheme(SelectedThemeRef ref) {
switch (ref.watch(appStatusProvider).selectedTheme) {
case SelectedTheme.system:
return SchedulerBinding.instance.window.platformBrightness == Brightness.light ? ThemeMode.light : ThemeMode.dark;
case SelectedTheme.light:
return ThemeMode.light;
case SelectedTheme.dark:
return ThemeMode.dark;
default:
return ThemeMode.light;
}
}

View File

@@ -6,21 +6,34 @@ part of 'app_status_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$appStatusProviderHash() => r'ba585a0cf3cf46f88f5895eacec3e992bf0cd42f';
String _$selectedThemeHash() => r'b820484a59ed72419c5b4cad0b894a030b54c702';
/// See also [AppStatusProvider].
@ProviderFor(AppStatusProvider)
final appStatusProviderProvider =
NotifierProvider<AppStatusProvider, AppStatus>.internal(
AppStatusProvider.new,
name: r'appStatusProviderProvider',
/// See also [selectedTheme].
@ProviderFor(selectedTheme)
final selectedThemeProvider = AutoDisposeProvider<ThemeMode>.internal(
selectedTheme,
name: r'selectedThemeProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appStatusProviderHash,
: _$selectedThemeHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppStatusProvider = Notifier<AppStatus>;
typedef SelectedThemeRef = AutoDisposeProviderRef<ThemeMode>;
String _$appStatusHash() => r'cde4567157dd5a89f842a2818efc199db3585232';
/// See also [AppStatus].
@ProviderFor(AppStatus)
final appStatusProvider = NotifierProvider<AppStatus, AppStatusModel>.internal(
AppStatus.new,
name: r'appStatusProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$appStatusHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppStatus = Notifier<AppStatusModel>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:linkdy/config/shared_preferences_keys.dart';
import 'package:linkdy/constants/shared_preferences_keys.dart';
import 'package:linkdy/providers/shared_preferences_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

View File

@@ -1,6 +1,9 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
part 'shared_preferences_provider.g.dart';
@Riverpod(keepAlive: true)
SharedPreferences sharedPreferences(SharedPreferencesRef ref) {
throw UnimplementedError();
});
}

View File

@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'shared_preferences_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$sharedPreferencesHash() => r'3a9f8412df34c1653d08100c9826aa2125b80f7f';
/// See also [sharedPreferences].
@ProviderFor(sharedPreferences)
final sharedPreferencesProvider = Provider<SharedPreferences>.internal(
sharedPreferences,
name: r'sharedPreferencesProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$sharedPreferencesHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SharedPreferencesRef = ProviderRef<SharedPreferences>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -4,4 +4,5 @@ class RoutesPaths {
static const webview = "/webview";
static const search = "/search";
static const settings = "/settings";
static const customization = "/settings/customization";
}

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:linkdy/screens/links/ui/links.dart';
import 'package:linkdy/screens/onboarding/ui/onboarding.dart';
import 'package:linkdy/screens/search/ui/search.dart';
import 'package:linkdy/screens/settings/ui/customization/customization.dart';
import 'package:linkdy/screens/settings/ui/settings.dart';
import 'package:linkdy/screens/webview/ui/webview.dart';
import 'package:linkdy/widgets/layout.dart';
@@ -58,6 +59,10 @@ final List<RouteBase> appRoutes = [
path: RoutesPaths.settings,
builder: (context, state) => const Settings(),
),
GoRoute(
path: RoutesPaths.customization,
builder: (context, state) => const Customization(),
),
],
),
],

View File

@@ -0,0 +1,9 @@
import 'package:flutter/material.dart';
class CustomizationModel {
ScrollController? scrollController;
CustomizationModel({
this.scrollController,
});
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:linkdy/screens/settings/model/customization.model.dart';
part 'customization.provider.g.dart';
@riverpod
class Customization extends _$Customization {
@override
CustomizationModel build() {
return CustomizationModel(
scrollController: ScrollController(),
);
}
}

View File

@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'customization.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$customizationHash() => r'c4cf4b566abbe697ec14c5024e1576f230d3e9fb';
/// See also [Customization].
@ProviderFor(Customization)
final customizationProvider =
AutoDisposeNotifierProvider<Customization, CustomizationModel>.internal(
Customization.new,
name: r'customizationProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$customizationHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Customization = AutoDisposeNotifier<CustomizationModel>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
class ColorItem extends StatelessWidget {
final int index;
final int total;
final Color color;
final int numericValue;
final int? selectedValue;
final void Function(int) onChanged;
const ColorItem({
super.key,
required this.index,
required this.total,
required this.color,
required this.numericValue,
required this.selectedValue,
required this.onChanged
});
@override
Widget build(BuildContext context) {
return Padding(
padding: index == 0
? const EdgeInsets.only(top: 10, right: 10, bottom: 10)
: index == total-1
? const EdgeInsets.only(top: 10, bottom: 10, left: 10)
: const EdgeInsets.all(10),
child: Material(
borderRadius: BorderRadius.circular(50),
child: InkWell(
onTap: () => onChanged(numericValue),
borderRadius: BorderRadius.circular(50),
overlayColor: const MaterialStatePropertyAll(Colors.grey),
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(50)
),
child: AnimatedOpacity(
opacity: numericValue == selectedValue ? 1 : 0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Icon(
Icons.check,
size: 30,
color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,156 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:linkdy/screens/settings/provider/customization.provider.dart';
import 'package:linkdy/screens/settings/ui/customization/color_item.dart';
import 'package:linkdy/screens/settings/ui/customization/theme_mode_button.dart';
import 'package:linkdy/utils/color_translation.dart';
import 'package:linkdy/widgets/custom_switch_list_tile.dart';
import 'package:linkdy/widgets/section_label.dart';
import 'package:linkdy/constants/colors.dart';
import 'package:linkdy/constants/enums.dart';
import 'package:linkdy/i18n/strings.g.dart';
import 'package:linkdy/providers/app_status_provider.dart';
class Customization extends ConsumerWidget {
const Customization({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text(t.settings.customization.customization),
centerTitle: false,
),
body: SafeArea(
child: ListView(
children: [
SectionLabel(
label: t.settings.customization.theme,
padding: const EdgeInsets.only(top: 10, left: 16, right: 16, bottom: 5),
),
Column(
children: [
CustomSwitchListTile(
value: ref.watch(appStatusProvider).selectedTheme == SelectedTheme.system ? true : false,
onChanged: (value) => ref
.read(appStatusProvider.notifier)
.setTheme(value == true ? SelectedTheme.system : SelectedTheme.light),
title: t.settings.customization.systemDefined,
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ThemeModeButton(
icon: Icons.light_mode,
value: 1,
selected: ref.watch(appStatusProvider).selectedTheme.index,
label: t.settings.customization.light,
onChanged: (value) => ref.read(appStatusProvider.notifier).setTheme(SelectedTheme.values[value]),
disabled: ref.watch(appStatusProvider).selectedTheme == SelectedTheme.system ? true : false,
),
ThemeModeButton(
icon: Icons.dark_mode,
value: 2,
selected: ref.watch(appStatusProvider).selectedTheme.index,
label: t.settings.customization.dark,
onChanged: (value) => ref.read(appStatusProvider.notifier).setTheme(SelectedTheme.values[value]),
disabled: ref.watch(appStatusProvider).selectedTheme == SelectedTheme.system ? true : false,
),
],
),
],
),
SectionLabel(
label: t.settings.customization.color,
padding: const EdgeInsets.only(top: 45, left: 16, right: 16, bottom: 5),
),
if (ref.watch(appStatusProvider).supportsDynamicTheme)
CustomSwitchListTile(
value: ref.watch(appStatusProvider).useDynamicTheme,
onChanged: ref.read(appStatusProvider.notifier).setUseDynamicTheme,
title: t.settings.customization.useDynamicTheme,
),
if (ref.watch(appStatusProvider).supportsDynamicTheme == false ||
(ref.watch(appStatusProvider).supportsDynamicTheme == true &&
ref.watch(appStatusProvider).useDynamicTheme == false))
Padding(
padding: const EdgeInsets.only(bottom: 8, left: 16, right: 16),
child: Scrollbar(
controller: ref.watch(customizationProvider).scrollController,
thumbVisibility: Platform.isMacOS || Platform.isLinux || Platform.isWindows,
interactive: Platform.isMacOS || Platform.isLinux || Platform.isWindows,
thickness: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 8 : 0,
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 70,
child: ListView.builder(
controller: ref.watch(customizationProvider).scrollController,
scrollDirection: Axis.horizontal,
itemCount: colors.length,
padding: const EdgeInsets.all(0),
itemBuilder: (context, index) {
if (index == 0) {
return Row(
children: [
ColorItem(
index: index,
total: colors.length,
color: colors[index],
numericValue: index,
selectedValue: ref.watch(appStatusProvider).selectedColor,
onChanged: ref.read(appStatusProvider.notifier).setSelectedColor,
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
width: 1,
height: 60,
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(1),
),
),
],
);
} else {
return ColorItem(
index: index,
total: colors.length,
color: colors[index],
numericValue: index,
selectedValue: ref.watch(appStatusProvider).selectedColor,
onChanged: ref.read(appStatusProvider.notifier).setSelectedColor,
);
}
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
colorTranslation(ref.watch(appStatusProvider).selectedColor),
style: TextStyle(color: Theme.of(context).listTileTheme.iconColor, fontSize: 16),
),
),
],
),
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
class ThemeModeButton extends StatelessWidget {
final IconData icon;
final int value;
final int selected;
final String label;
final void Function(int) onChanged;
final bool? disabled;
const ThemeModeButton({
Key? key,
required this.icon,
required this.value,
required this.selected,
required this.label,
required this.onChanged,
this.disabled
}) : super(key: key);
@override
Widget build(BuildContext context) {
final Color greyBackgroundColor = Theme.of(context).brightness == Brightness.light
?const Color.fromRGBO(200, 200, 200, 1)
:const Color.fromRGBO(50, 50, 50, 1);
final Color greyIconColor = Theme.of(context).brightness == Brightness.light
? const Color.fromRGBO(130, 130, 130, 1)
: const Color.fromRGBO(100, 100, 100, 1);
return ElevatedButton(
onPressed: disabled == null || disabled == false
? () => onChanged(value)
: null,
style: ButtonStyle(
elevation: MaterialStateProperty.all(0),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
)
),
backgroundColor: MaterialStateProperty.all(
value == selected
? disabled == null || disabled == false
? Theme.of(context).colorScheme.primary
: greyBackgroundColor
: disabled == null || disabled == false
? Theme.of(context).colorScheme.surfaceVariant
: greyBackgroundColor,
)
),
child: AnimatedContainer(
width: 118,
height: 150,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 200),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(
icon,
color: value == selected
? disabled == null || disabled == false
? Theme.of(context).colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white
: greyIconColor
: disabled == null || disabled == false
? Theme.of(context).colorScheme.primary
: greyIconColor,
size: 30,
),
Text(
label,
style: TextStyle(
color: value == selected
? disabled == null || disabled == false
? Theme.of(context).colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white
: greyIconColor
: disabled == null || disabled == false
? Theme.of(context).colorScheme.primary
: greyIconColor,
fontSize: 18
),
)
],
),
),
);
}
}

View File

@@ -1,6 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:linkdy/providers/router_provider.dart';
import 'package:linkdy/router/paths.dart';
import 'package:linkdy/widgets/section_label.dart';
import 'package:linkdy/widgets/custom_list_tile.dart';
import 'package:linkdy/providers/app_info_provider.dart';
import 'package:linkdy/utils/open_url.dart';
import 'package:linkdy/constants/strings.dart';
import 'package:linkdy/constants/urls.dart';
import 'package:linkdy/i18n/strings.g.dart';
import 'package:linkdy/providers/api_client_provider.dart';
@@ -15,6 +25,18 @@ class Settings extends ConsumerWidget {
),
body: ListView(
children: [
SectionLabel(label: t.settings.appSettings),
CustomListTile(
icon: Icons.palette_rounded,
title: t.settings.customization.customization,
subtitle: t.settings.customizationDescription,
onTap: () => ref.read(routerProvider).push(RoutesPaths.customization),
),
CustomListTile(
icon: Icons.settings_rounded,
title: t.settings.generalSettings,
subtitle: t.settings.generalSettingsDescription,
),
FilledButton.icon(
onPressed: () => ref.read(apiClientProviderProvider.notifier).disconnectApiClient(),
icon: const Icon(Icons.clear_rounded),
@@ -22,6 +44,44 @@ class Settings extends ConsumerWidget {
t.settings.disconnectFromServer,
),
),
SectionLabel(label: t.settings.aboutApp),
CustomListTile(
title: t.settings.appVersion,
subtitle: ref.watch(appInfoProvider).version,
),
CustomListTile(
title: t.settings.createdBy,
subtitle: Strings.createdBy,
),
Padding(
padding: const EdgeInsets.all(15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// if (Platform.isAndroid)
// IconButton(
// onPressed: () => openUrl(Urls.playStore),
// icon: SvgPicture.asset(
// 'assets/resources/google-play.svg',
// color: Theme.of(context).colorScheme.onSurfaceVariant,
// width: 30,
// height: 30,
// ),
// tooltip: t.settings.visitGooglePlay,
// ),
IconButton(
onPressed: () => openUrl(Urls.gitHubRepo),
icon: SvgPicture.asset(
'assets/resources/github.svg',
color: Theme.of(context).colorScheme.onSurfaceVariant,
width: 30,
height: 30,
),
tooltip: t.settings.visitGitHubRepo,
),
],
),
),
],
),
);

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:linkdy/i18n/strings.g.dart';
String colorTranslation(int index) {
// This indexes has to be in sync with colors array at constants/colors.dart
List<String> translations = [
"Linkding",
t.colors.red,
t.colors.green,
t.colors.blue,
t.colors.yellow,
t.colors.orange,
t.colors.brown,
t.colors.cyan,
t.colors.purple,
t.colors.pink,
t.colors.deepOrange,
t.colors.indigo,
];
return translations[index];
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
class CustomListTile extends StatelessWidget {
final String title;
final String? subtitle;
final Widget? subtitleWidget;
final void Function()? onTap;
final IconData? icon;
final Widget? trailing;
final EdgeInsets? padding;
final void Function()? onLongPress;
final bool? disabled;
final void Function(bool)? onHover;
final Color? color;
const CustomListTile({
Key? key,
required this.title,
this.subtitle,
this.subtitleWidget,
this.onTap,
this.icon,
this.trailing,
this.padding,
this.onLongPress,
this.disabled,
this.onHover,
this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: color ?? Colors.transparent,
child: InkWell(
onTap: onTap,
onHover: onHover,
onLongPress: onLongPress,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(
icon,
size: 24,
color: disabled == true
? Theme.of(context).colorScheme.onSurface.withOpacity(0.38)
: Theme.of(context).listTileTheme.iconColor,
),
const SizedBox(width: 16),
],
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: disabled == true
? Theme.of(context).colorScheme.onSurface.withOpacity(0.38)
: Theme.of(context).colorScheme.onSurface,
),
),
if (subtitle != null || subtitleWidget != null) ...[
const SizedBox(height: 5),
if (subtitle == null && subtitleWidget != null) subtitleWidget!,
if (subtitle != null && subtitleWidget == null)
Text(
subtitle!,
style: TextStyle(
color: disabled == true
? Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.38)
: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 14,
fontWeight: FontWeight.w400),
),
]
],
),
)
],
),
),
if (trailing != null) ...[const SizedBox(width: 16), trailing!]
],
),
),
),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
class CustomSwitchListTile extends StatelessWidget {
final bool value;
final void Function(bool) onChanged;
final String title;
final String? subtitle;
final bool? disabled;
final EdgeInsets? padding;
final IconData? leadingIcon;
const CustomSwitchListTile({
super.key,
required this.value,
required this.onChanged,
required this.title,
this.disabled,
this.subtitle,
this.padding,
this.leadingIcon,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: disabled != null && disabled == true
? null
: () => onChanged(!value),
child: Padding(
padding: padding ?? const EdgeInsets.only(
top: 12, left: 16, right: 18, bottom: 16
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (leadingIcon != null) ...[
Icon(
leadingIcon,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 16),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
color: disabled != null && disabled == true
? Theme.of(context).colorScheme.onSurface.withOpacity(0.38)
: Theme.of(context).colorScheme.onSurface,
),
),
if (subtitle != null) ... [
const SizedBox(height: 5),
SizedBox(
width: MediaQuery.of(context).size.width-110,
child: Text(
subtitle!,
style: TextStyle(
fontSize: 14,
color: disabled != null && disabled == true
? Theme.of(context).listTileTheme.textColor!.withOpacity(0.38)
: Theme.of(context).listTileTheme.textColor
),
),
),
]
],
),
),
const SizedBox(width: 16),
Switch(
value: value,
onChanged: disabled != null && disabled == true
? null
: onChanged,
)
],
),
),
),
);
}
}

View File

@@ -7,6 +7,7 @@ import Foundation
import dynamic_color
import flutter_inappwebview_macos
import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
@@ -15,6 +16,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View File

@@ -661,6 +661,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79"
url: "https://pub.dev"
source: hosted
version: "5.0.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
path:
dependency: transitive
description:

View File

@@ -49,6 +49,7 @@ dependencies:
flutter_custom_tabs: ^2.0.0+1
flutter_inappwebview: ^6.0.0
share_plus: ^7.2.2
package_info_plus: ^5.0.1
dev_dependencies:
build_runner: ^2.4.8