Created onboarding
This commit is contained in:
1
assets/resources/github.svg
Normal file
1
assets/resources/github.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
assets/resources/google-play.svg
Normal file
1
assets/resources/google-play.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 588 B |
@@ -4,9 +4,9 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 2
|
/// Locales: 2
|
||||||
/// Strings: 8 (4 per locale)
|
/// Strings: 30 (15 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2024-02-21 at 23:50 UTC
|
/// Built on 2024-02-22 at 01:09 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
@@ -148,8 +148,28 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
|
|||||||
late final Translations _root = this; // ignore: unused_field
|
late final Translations _root = this; // ignore: unused_field
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
|
late final _StringsOnboardingEn onboarding = _StringsOnboardingEn._(_root);
|
||||||
late final _StringsConnectEn connect = _StringsConnectEn._(_root);
|
late final _StringsConnectEn connect = _StringsConnectEn._(_root);
|
||||||
late final _StringsLinksEn links = _StringsLinksEn._(_root);
|
late final _StringsLinksEn links = _StringsLinksEn._(_root);
|
||||||
|
late final _StringsSettingsEn settings = _StringsSettingsEn._(_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: onboarding
|
||||||
|
class _StringsOnboardingEn {
|
||||||
|
_StringsOnboardingEn._(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
String get title => 'Welcome to My Linkding';
|
||||||
|
String get subtitle => 'An application to manage your bookmarks.';
|
||||||
|
String get start => 'Start';
|
||||||
|
String get next => 'Next';
|
||||||
|
String get previous => 'Previous';
|
||||||
|
String get serverRequired => 'Servidor requerido';
|
||||||
|
String get serverRequiredDescription => 'My Linkding it\'s not an standalone app, it requires the Linkding server to work.\nIn order to use this application, you must deploy Linkding on your home server, VPS or any other computer.';
|
||||||
|
String get installationInstructions => 'Check the installation instructions on the official GitHub repository.';
|
||||||
|
String get serverRunningConfirmation => 'I confirm that I have an instance of the Linkding server already running.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: connect
|
// Path: connect
|
||||||
@@ -174,6 +194,17 @@ class _StringsLinksEn {
|
|||||||
String get links => 'Links';
|
String get links => 'Links';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: settings
|
||||||
|
class _StringsSettingsEn {
|
||||||
|
_StringsSettingsEn._(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
String get settings => 'Settings';
|
||||||
|
String get disconnectFromServer => 'Disconnect from server';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: <root>
|
// Path: <root>
|
||||||
class _StringsEs implements Translations {
|
class _StringsEs implements Translations {
|
||||||
/// You can call this constructor and build your own translation instance of this locale.
|
/// You can call this constructor and build your own translation instance of this locale.
|
||||||
@@ -198,8 +229,28 @@ class _StringsEs implements Translations {
|
|||||||
@override late final _StringsEs _root = this; // ignore: unused_field
|
@override late final _StringsEs _root = this; // ignore: unused_field
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
|
@override late final _StringsOnboardingEs onboarding = _StringsOnboardingEs._(_root);
|
||||||
@override late final _StringsConnectEs connect = _StringsConnectEs._(_root);
|
@override late final _StringsConnectEs connect = _StringsConnectEs._(_root);
|
||||||
@override late final _StringsLinksEs links = _StringsLinksEs._(_root);
|
@override late final _StringsLinksEs links = _StringsLinksEs._(_root);
|
||||||
|
@override late final _StringsSettingsEs settings = _StringsSettingsEs._(_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: onboarding
|
||||||
|
class _StringsOnboardingEs implements _StringsOnboardingEn {
|
||||||
|
_StringsOnboardingEs._(this._root);
|
||||||
|
|
||||||
|
@override final _StringsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Bienvenido a My Linkding';
|
||||||
|
@override String get subtitle => 'Una aplicación para gestionar tus enlaces favoritos.';
|
||||||
|
@override String get start => 'Comenzar';
|
||||||
|
@override String get next => 'Siguiente';
|
||||||
|
@override String get previous => 'Anterior';
|
||||||
|
@override String get serverRequired => 'Servidor requerido';
|
||||||
|
@override String get serverRequiredDescription => 'My Linkding no es una aplicación independiente, requiere el servidor Linkding para funcionar.\nPara utilizar esta aplicación, debe instalar Linkding en su servidor doméstico, VPS o cualquier otro ordenador.';
|
||||||
|
@override String get installationInstructions => 'Mira las instrucciones de instalación en el repositorio oficial en GitHub.';
|
||||||
|
@override String get serverRunningConfirmation => 'Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: connect
|
// Path: connect
|
||||||
@@ -224,16 +275,38 @@ class _StringsLinksEs implements _StringsLinksEn {
|
|||||||
@override String get links => 'Enlaces';
|
@override String get links => 'Enlaces';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: settings
|
||||||
|
class _StringsSettingsEs implements _StringsSettingsEn {
|
||||||
|
_StringsSettingsEs._(this._root);
|
||||||
|
|
||||||
|
@override final _StringsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get settings => 'Ajustes';
|
||||||
|
@override String get disconnectFromServer => 'Desconectar del servidor';
|
||||||
|
}
|
||||||
|
|
||||||
/// Flat map(s) containing all translations.
|
/// Flat map(s) containing all translations.
|
||||||
/// Only for edge cases! For simple maps, use the map function of this library.
|
/// Only for edge cases! For simple maps, use the map function of this library.
|
||||||
|
|
||||||
extension on Translations {
|
extension on Translations {
|
||||||
dynamic _flatMapFunction(String path) {
|
dynamic _flatMapFunction(String path) {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
|
case 'onboarding.title': return 'Welcome to My Linkding';
|
||||||
|
case 'onboarding.subtitle': return 'An application to manage your bookmarks.';
|
||||||
|
case 'onboarding.start': return 'Start';
|
||||||
|
case 'onboarding.next': return 'Next';
|
||||||
|
case 'onboarding.previous': return 'Previous';
|
||||||
|
case 'onboarding.serverRequired': return 'Servidor requerido';
|
||||||
|
case 'onboarding.serverRequiredDescription': return 'My Linkding it\'s not an standalone app, it requires the Linkding server to work.\nIn order to use this application, you must deploy Linkding on your home server, VPS or any other computer.';
|
||||||
|
case 'onboarding.installationInstructions': return 'Check the installation instructions on the official GitHub repository.';
|
||||||
|
case 'onboarding.serverRunningConfirmation': return 'I confirm that I have an instance of the Linkding server already running.';
|
||||||
case 'connect.connect': return 'Connect';
|
case 'connect.connect': return 'Connect';
|
||||||
case 'connect.connecting': return 'Connecting...';
|
case 'connect.connecting': return 'Connecting...';
|
||||||
case 'connect.cannotConnectToServer': return 'Cannot connect to the server.';
|
case 'connect.cannotConnectToServer': return 'Cannot connect to the server.';
|
||||||
case 'links.links': return 'Links';
|
case 'links.links': return 'Links';
|
||||||
|
case 'settings.settings': return 'Settings';
|
||||||
|
case 'settings.disconnectFromServer': return 'Disconnect from server';
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,10 +315,21 @@ extension on Translations {
|
|||||||
extension on _StringsEs {
|
extension on _StringsEs {
|
||||||
dynamic _flatMapFunction(String path) {
|
dynamic _flatMapFunction(String path) {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
|
case 'onboarding.title': return 'Bienvenido a My Linkding';
|
||||||
|
case 'onboarding.subtitle': return 'Una aplicación para gestionar tus enlaces favoritos.';
|
||||||
|
case 'onboarding.start': return 'Comenzar';
|
||||||
|
case 'onboarding.next': return 'Siguiente';
|
||||||
|
case 'onboarding.previous': return 'Anterior';
|
||||||
|
case 'onboarding.serverRequired': return 'Servidor requerido';
|
||||||
|
case 'onboarding.serverRequiredDescription': return 'My Linkding no es una aplicación independiente, requiere el servidor Linkding para funcionar.\nPara utilizar esta aplicación, debe instalar Linkding en su servidor doméstico, VPS o cualquier otro ordenador.';
|
||||||
|
case 'onboarding.installationInstructions': return 'Mira las instrucciones de instalación en el repositorio oficial en GitHub.';
|
||||||
|
case 'onboarding.serverRunningConfirmation': return 'Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento.';
|
||||||
case 'connect.connect': return 'Conectar';
|
case 'connect.connect': return 'Conectar';
|
||||||
case 'connect.connecting': return 'Conectando...';
|
case 'connect.connecting': return 'Conectando...';
|
||||||
case 'connect.cannotConnectToServer': return 'No se puede conectar con el servidor.';
|
case 'connect.cannotConnectToServer': return 'No se puede conectar con el servidor.';
|
||||||
case 'links.links': return 'Enlaces';
|
case 'links.links': return 'Enlaces';
|
||||||
|
case 'settings.settings': return 'Ajustes';
|
||||||
|
case 'settings.disconnectFromServer': return 'Desconectar del servidor';
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"onboarding": {
|
||||||
|
"title": "Welcome to My Linkding",
|
||||||
|
"subtitle": "An application to manage your bookmarks.",
|
||||||
|
"start": "Start",
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Previous",
|
||||||
|
"serverRequired": "Servidor requerido",
|
||||||
|
"serverRequiredDescription": "My Linkding it's not an standalone app, it requires the Linkding server to work.\nIn order to use this application, you must deploy Linkding on your home server, VPS or any other computer.",
|
||||||
|
"installationInstructions": "Check the installation instructions on the official GitHub repository.",
|
||||||
|
"serverRunningConfirmation": "I confirm that I have an instance of the Linkding server already running."
|
||||||
|
},
|
||||||
"connect": {
|
"connect": {
|
||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
"connecting": "Connecting...",
|
"connecting": "Connecting...",
|
||||||
@@ -6,5 +17,9 @@
|
|||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"links": "Links"
|
"links": "Links"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings": "Settings",
|
||||||
|
"disconnectFromServer": "Disconnect from server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"onboarding": {
|
||||||
|
"title": "Bienvenido a My Linkding",
|
||||||
|
"subtitle": "Una aplicación para gestionar tus enlaces favoritos.",
|
||||||
|
"start": "Comenzar",
|
||||||
|
"next": "Siguiente",
|
||||||
|
"previous": "Anterior",
|
||||||
|
"serverRequired": "Servidor requerido",
|
||||||
|
"serverRequiredDescription": "My Linkding no es una aplicación independiente, requiere el servidor Linkding para funcionar.\nPara utilizar esta aplicación, debe instalar Linkding en su servidor doméstico, VPS o cualquier otro ordenador.",
|
||||||
|
"installationInstructions": "Mira las instrucciones de instalación en el repositorio oficial en GitHub.",
|
||||||
|
"serverRunningConfirmation": "Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento."
|
||||||
|
},
|
||||||
"connect": {
|
"connect": {
|
||||||
"connect": "Conectar",
|
"connect": "Conectar",
|
||||||
"connecting": "Conectando...",
|
"connecting": "Conectando...",
|
||||||
@@ -6,5 +17,9 @@
|
|||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"links": "Enlaces"
|
"links": "Enlaces"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings": "Ajustes",
|
||||||
|
"disconnectFromServer": "Desconectar del servidor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,4 +18,9 @@ class ApiClientProvider extends _$ApiClientProvider {
|
|||||||
void setApiClient(ApiClient client) {
|
void setApiClient(ApiClient client) {
|
||||||
state = client;
|
state = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void disconnectApiClient() {
|
||||||
|
state = null;
|
||||||
|
ref.read(serverInstancesProvider.notifier).removeServerInstances();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'api_client_provider.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$apiClientProviderHash() => r'eff6aeacbfe369f2204d77f4884cc9d78ecf1e27';
|
String _$apiClientProviderHash() => r'8c44873c4e0758acfcd64b181134b80a43327f46';
|
||||||
|
|
||||||
/// See also [ApiClientProvider].
|
/// See also [ApiClientProvider].
|
||||||
@ProviderFor(ApiClientProvider)
|
@ProviderFor(ApiClientProvider)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ GoRouter router(RouterRef ref) {
|
|||||||
return GoRouter(
|
return GoRouter(
|
||||||
redirect: (context, state) {
|
redirect: (context, state) {
|
||||||
if (ref.watch(apiClientProviderProvider) == null) {
|
if (ref.watch(apiClientProviderProvider) == null) {
|
||||||
return RoutesPaths.connect;
|
return RoutesPaths.onboarding;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'router_provider.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$routerHash() => r'47e3f3d747dcb7b2543c73209f8a2f5561eb6ff7';
|
String _$routerHash() => r'78eb4c32cddebb29e3029e0eeaeb0f4c40d77012';
|
||||||
|
|
||||||
/// See also [router].
|
/// See also [router].
|
||||||
@ProviderFor(router)
|
@ProviderFor(router)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import 'package:my_linkding/config/shared_preferences_keys.dart';
|
|||||||
import 'package:my_linkding/providers/shared_preferences_provider.dart';
|
import 'package:my_linkding/providers/shared_preferences_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
import 'package:my_linkding/services/api_client.dart';
|
|
||||||
import 'package:my_linkding/providers/api_client_provider.dart';
|
|
||||||
import 'package:my_linkding/models/server_instance.dart';
|
import 'package:my_linkding/models/server_instance.dart';
|
||||||
|
|
||||||
part 'server_instances_provider.g.dart';
|
part 'server_instances_provider.g.dart';
|
||||||
@@ -45,4 +43,9 @@ class ServerInstances extends _$ServerInstances {
|
|||||||
final stringList = instances.map((i) => jsonEncode(i.toJson())).toList();
|
final stringList = instances.map((i) => jsonEncode(i.toJson())).toList();
|
||||||
ref.read(sharedPreferencesProvider).setStringList(SharedPreferencesKeys.serversInstances, stringList);
|
ref.read(sharedPreferencesProvider).setStringList(SharedPreferencesKeys.serversInstances, stringList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void removeServerInstances() {
|
||||||
|
state = [];
|
||||||
|
ref.read(sharedPreferencesProvider).remove(SharedPreferencesKeys.serversInstances);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'server_instances_provider.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$serverInstancesHash() => r'82b7d3367afbe5c19ad0238977e5df35fac3499b';
|
String _$serverInstancesHash() => r'ee41f5a717b9f779aa4319cc711b479a47c1c9ef';
|
||||||
|
|
||||||
/// See also [ServerInstances].
|
/// See also [ServerInstances].
|
||||||
@ProviderFor(ServerInstances)
|
@ProviderFor(ServerInstances)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
class RoutesPaths {
|
class RoutesPaths {
|
||||||
static const connect = "/connect";
|
static const onboarding = "/onboarding";
|
||||||
static const links = "/links";
|
static const links = "/links";
|
||||||
static const search = "/search";
|
static const search = "/search";
|
||||||
static const settings = "/settings";
|
static const settings = "/settings";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import 'package:my_linkding/screens/connect/ui/connect.dart';
|
|
||||||
import 'package:my_linkding/screens/links/ui/links.dart';
|
import 'package:my_linkding/screens/links/ui/links.dart';
|
||||||
|
import 'package:my_linkding/screens/onboarding/ui/onboarding.dart';
|
||||||
import 'package:my_linkding/screens/search/ui/search.dart';
|
import 'package:my_linkding/screens/search/ui/search.dart';
|
||||||
import 'package:my_linkding/screens/settings/ui/settings.dart';
|
import 'package:my_linkding/screens/settings/ui/settings.dart';
|
||||||
import 'package:my_linkding/widgets/layout.dart';
|
import 'package:my_linkding/widgets/layout.dart';
|
||||||
@@ -16,8 +16,8 @@ final settingsNavigatorKey = GlobalKey<NavigatorState>();
|
|||||||
|
|
||||||
final List<RouteBase> appRoutes = [
|
final List<RouteBase> appRoutes = [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RoutesPaths.connect,
|
path: RoutesPaths.onboarding,
|
||||||
builder: (context, state) => const Connect(),
|
builder: (context, state) => const Onboarding(),
|
||||||
),
|
),
|
||||||
StatefulShellRoute.indexedStack(
|
StatefulShellRoute.indexedStack(
|
||||||
builder: (context, state, navigationShell) => Layout(
|
builder: (context, state, navigationShell) => Layout(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'connect_provider.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$connectToServerHash() => r'af66fee9a4bf26aba08b27ff5a7fe4643215117a';
|
String _$connectToServerHash() => r'c40898fd5dd6dd7507140c7a69a6f336a56e30c0';
|
||||||
|
|
||||||
/// See also [connectToServer].
|
/// See also [connectToServer].
|
||||||
@ProviderFor(connectToServer)
|
@ProviderFor(connectToServer)
|
||||||
|
|||||||
16
lib/screens/onboarding/model/onboarding.model.dart
Normal file
16
lib/screens/onboarding/model/onboarding.model.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class OnboardingModel {
|
||||||
|
final PageController pageController;
|
||||||
|
final bool confirmServerRunning;
|
||||||
|
|
||||||
|
OnboardingModel({
|
||||||
|
required this.pageController,
|
||||||
|
required this.confirmServerRunning,
|
||||||
|
});
|
||||||
|
|
||||||
|
OnboardingModel toggleConfirmServerRunning(bool newValue) => OnboardingModel(
|
||||||
|
pageController: pageController,
|
||||||
|
confirmServerRunning: newValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
35
lib/screens/onboarding/provider/onboarding.provider.dart
Normal file
35
lib/screens/onboarding/provider/onboarding.provider.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:my_linkding/screens/onboarding/model/onboarding.model.dart';
|
||||||
|
|
||||||
|
part 'onboarding.provider.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class Onboarding extends _$Onboarding {
|
||||||
|
@override
|
||||||
|
OnboardingModel build() {
|
||||||
|
return OnboardingModel(
|
||||||
|
pageController: PageController(),
|
||||||
|
confirmServerRunning: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nextPage() {
|
||||||
|
state.pageController.nextPage(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void previousPage() {
|
||||||
|
state.pageController.previousPage(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void confirmServerRunning() {
|
||||||
|
state = state.toggleConfirmServerRunning(!state.confirmServerRunning);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/screens/onboarding/provider/onboarding.provider.g.dart
Normal file
25
lib/screens/onboarding/provider/onboarding.provider.g.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'onboarding.provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$onboardingHash() => r'5c876d53acbca92ab15630516099700609aa4f89';
|
||||||
|
|
||||||
|
/// See also [Onboarding].
|
||||||
|
@ProviderFor(Onboarding)
|
||||||
|
final onboardingProvider =
|
||||||
|
AutoDisposeNotifierProvider<Onboarding, OnboardingModel>.internal(
|
||||||
|
Onboarding.new,
|
||||||
|
name: r'onboardingProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$onboardingHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$Onboarding = AutoDisposeNotifier<OnboardingModel>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
29
lib/screens/onboarding/ui/onboarding.dart
Normal file
29
lib/screens/onboarding/ui/onboarding.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart';
|
||||||
|
import 'package:my_linkding/screens/onboarding/ui/server_required.dart';
|
||||||
|
import 'package:my_linkding/screens/onboarding/ui/welcome.dart';
|
||||||
|
import 'package:my_linkding/widgets/system_overlay_style.dart';
|
||||||
|
|
||||||
|
class Onboarding extends ConsumerWidget {
|
||||||
|
const Onboarding({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return OverlayStyle(
|
||||||
|
child: Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: PageView(
|
||||||
|
controller: ref.watch(onboardingProvider).pageController,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: const [
|
||||||
|
OnboardingWelcome(),
|
||||||
|
OnboardingServerRequired(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
lib/screens/onboarding/ui/server_required.dart
Normal file
132
lib/screens/onboarding/ui/server_required.dart
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
|
import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart';
|
||||||
|
|
||||||
|
import 'package:my_linkding/i18n/strings.g.dart';
|
||||||
|
|
||||||
|
class OnboardingServerRequired extends ConsumerWidget {
|
||||||
|
const OnboardingServerRequired({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.dns_rounded,
|
||||||
|
size: 50,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
t.onboarding.serverRequired,
|
||||||
|
style: const TextStyle(fontSize: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
t.onboarding.serverRequiredDescription,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: 24),
|
||||||
|
Card(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => {},
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/resources/github.svg',
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
t.onboarding.installationInstructions,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(8),
|
||||||
|
leading: Checkbox(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
value: ref.watch(onboardingProvider).confirmServerRunning,
|
||||||
|
onChanged: (_) => ref.read(onboardingProvider.notifier).confirmServerRunning(),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
t.onboarding.serverRunningConfirmation,
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
onTap: () => ref.read(onboardingProvider.notifier).confirmServerRunning(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => ref.read(onboardingProvider.notifier).previousPage(),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.arrow_back_rounded),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(t.onboarding.previous),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: ref.watch(onboardingProvider).confirmServerRunning == true
|
||||||
|
? () => ref.read(onboardingProvider.notifier).nextPage()
|
||||||
|
: null,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(t.onboarding.next),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.arrow_forward_rounded),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/screens/onboarding/ui/welcome.dart
Normal file
79
lib/screens/onboarding/ui/welcome.dart
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:my_linkding/i18n/strings.g.dart';
|
||||||
|
import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart';
|
||||||
|
|
||||||
|
class OnboardingWelcome extends ConsumerWidget {
|
||||||
|
const OnboardingWelcome({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final height = MediaQuery.of(context).size.height;
|
||||||
|
|
||||||
|
final iconSize = width > height ? height : width;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
t.onboarding.title,
|
||||||
|
style: const TextStyle(fontSize: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
t.onboarding.subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const SizedBox(),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => ref.read(onboardingProvider.notifier).nextPage(),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(t.onboarding.start),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.arrow_forward_rounded),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: 0.7,
|
||||||
|
child: Icon(
|
||||||
|
Icons.link_rounded,
|
||||||
|
size: iconSize - 48,
|
||||||
|
color: Theme.of(context).colorScheme.secondary.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,28 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class Settings extends StatelessWidget {
|
import 'package:my_linkding/i18n/strings.g.dart';
|
||||||
|
import 'package:my_linkding/providers/api_client_provider.dart';
|
||||||
|
|
||||||
|
class Settings extends ConsumerWidget {
|
||||||
const Settings({Key? key}) : super(key: key);
|
const Settings({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Material(
|
return Scaffold(
|
||||||
color: Colors.transparent,
|
appBar: AppBar(
|
||||||
child: NestedScrollView(
|
title: Text(t.settings.settings),
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
),
|
||||||
SliverOverlapAbsorber(
|
body: ListView(
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
children: [
|
||||||
sliver: SliverAppBar.large(
|
FilledButton.icon(
|
||||||
pinned: true,
|
onPressed: () => ref.read(apiClientProviderProvider.notifier).disconnectApiClient(),
|
||||||
floating: true,
|
icon: const Icon(Icons.clear_rounded),
|
||||||
centerTitle: false,
|
label: Text(
|
||||||
forceElevated: innerBoxIsScrolled,
|
t.settings.disconnectFromServer,
|
||||||
title: Text("Settings"),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
body: SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) => CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
64
pubspec.lock
64
pubspec.lock
@@ -315,6 +315,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.10"
|
version: "2.4.10"
|
||||||
|
flutter_svg:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_svg
|
||||||
|
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.10+1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -373,6 +381,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.2.0"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -493,6 +509,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.3"
|
version: "1.8.3"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -517,6 +541,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -882,6 +914,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.3"
|
version: "4.3.3"
|
||||||
|
vector_graphics:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics
|
||||||
|
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.11+1"
|
||||||
|
vector_graphics_codec:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_codec
|
||||||
|
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.11+1"
|
||||||
|
vector_graphics_compiler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_compiler
|
||||||
|
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.11+1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -938,6 +994,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ dependencies:
|
|||||||
uuid: ^4.3.3
|
uuid: ^4.3.3
|
||||||
slang: ^3.29.0
|
slang: ^3.29.0
|
||||||
slang_flutter: ^3.29.0
|
slang_flutter: ^3.29.0
|
||||||
|
flutter_svg: ^2.0.9
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.8
|
build_runner: ^2.4.8
|
||||||
@@ -64,9 +65,9 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
assets:
|
||||||
# - images/a_dot_burr.jpeg
|
- assets/resources/github.svg
|
||||||
# - images/a_dot_ham.jpeg
|
- assets/resources/google-play.svg
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|||||||
Reference in New Issue
Block a user