initial commit

This commit is contained in:
Adrian Baumgart 2023-06-26 23:40:05 +02:00
commit 68130104da
No known key found for this signature in database
38 changed files with 1433 additions and 0 deletions

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
.metadata Normal file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
- platform: android
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Adrian Baumgart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Shlink Mobile App
> :warning: **This app is still under development, it's not intended to be used yet**
This is a mobile app built with Flutter to see and manage all shortened URLs created with [Shlink](https://shlink.io/).

29
analysis_options.yaml Normal file
View File

@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

71
android/app/build.gradle Normal file
View File

@ -0,0 +1,71 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dev.abmgrt.shlink_app"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 18 //flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.abmgrt.shlink_app">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,49 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="dev.abmgrt.shlink_app">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="shlink_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ProcessURLActivity"
android:label="Shorten URL"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay"
tools:targetApi="cupcake">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -0,0 +1,6 @@
package dev.abmgrt.shlink_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.abmgrt.shlink_app">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

31
android/build.gradle Normal file
View File

@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

11
android/settings.gradle Normal file
View File

@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

View File

@ -0,0 +1,33 @@
import 'package:shlink_app/API/Classes/ShortURL_DeviceLongUrls.dart';
import 'package:shlink_app/API/Classes/ShortURL_Meta.dart';
import 'package:shlink_app/API/Classes/ShortURL_VisitsSummary.dart';
class ShortURL {
String shortCode;
String shortUrl;
String longUrl;
ShortURL_DeviceLongUrls deviceLongUrls;
DateTime dateCreated;
ShortURL_VisitsSummary visitsSummary;
List<dynamic> tags;
ShortURL_Meta meta;
String? domain;
String? title;
bool crawlable;
ShortURL(this.shortCode, this.shortUrl, this.longUrl, this.deviceLongUrls, this.dateCreated, this.visitsSummary, this.tags, this.meta, this.domain, this.title, this.crawlable);
ShortURL.fromJson(Map<String, dynamic> json):
shortCode = json["shortCode"],
shortUrl = json["shortUrl"],
longUrl = json["longUrl"],
deviceLongUrls = ShortURL_DeviceLongUrls.fromJson(json["deviceLongUrls"]),
dateCreated = DateTime.parse(json["dateCreated"]),
visitsSummary = ShortURL_VisitsSummary.fromJson(json["visitsSummary"]),
tags = json["tags"],
meta = ShortURL_Meta.fromJson(json["meta"]),
domain = json["domain"],
title = json["title"],
crawlable = json["crawlable"];
}

View File

@ -0,0 +1,14 @@
import 'dart:convert';
class ShortURL_DeviceLongUrls {
final String? android;
final String? ios;
final String? desktop;
ShortURL_DeviceLongUrls(this.android, this.ios, this.desktop);
ShortURL_DeviceLongUrls.fromJson(Map<String, dynamic> json)
: android = json["android"],
ios = json["ios"],
desktop = json["desktop"];
}

View File

@ -0,0 +1,12 @@
class ShortURL_Meta {
DateTime? validSince;
DateTime? validUntil;
int? maxVisits;
ShortURL_Meta(this.validSince, this.validUntil, this.maxVisits);
ShortURL_Meta.fromJson(Map<String, dynamic> json):
validSince = json["validSince"] != null ? DateTime.parse(json["validSince"]) : null,
validUntil = json["validUntil"] != null ? DateTime.parse(json["validUntil"]) : null,
maxVisits = json["maxVisits"];
}

View File

@ -0,0 +1,12 @@
class ShortURL_VisitsSummary {
int total;
int nonBots;
int bots;
ShortURL_VisitsSummary(this.total, this.nonBots, this.bots);
ShortURL_VisitsSummary.fromJson(Map<String, dynamic> json):
total = json["total"] as int,
nonBots = json["nonBots"] as int,
bots = json["bots"] as int;
}

156
lib/API/ServerManager.dart Normal file
View File

@ -0,0 +1,156 @@
import 'dart:async';
import 'dart:convert';
import 'package:dartz/dartz.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:shlink_app/API/Classes/ShortURL.dart';
class ServerManager {
String? _server_url;
String? _api_key;
static String apiVersion = "3";
String getServerUrl() {
return _server_url ?? "";
}
Future<bool> checkLogin() async {
await _loadCredentials();
return (_server_url != null);
}
Future<void> _loadCredentials() async {
const storage = FlutterSecureStorage();
_server_url = await storage.read(key: "shlink_url");
_api_key = await storage.read(key: "shlink_apikey");
}
void _saveCredentials(String url, String apiKey) async {
const storage = FlutterSecureStorage();
storage.write(key: "shlink_url", value: url);
storage.write(key: "shlink_apikey", value: apiKey);
}
void _removeCredentials() async {
const storage = FlutterSecureStorage();
storage.delete(key: "shlink_url");
storage.delete(key: "shlink_apikey");
}
FutureOr<Either<String, Failure>> initAndConnect(String url, String apiKey) async {
// TODO: convert url to correct format
_server_url = url;
_api_key = apiKey;
_saveCredentials(url, apiKey);
final result = await connect();
result.fold((l) => null, (r) {
_removeCredentials();
});
return result;
}
FutureOr<Either<String, Failure>> connect() async {
_loadCredentials();
try {
final response = await http.get(Uri.parse("${_server_url}/rest/v${apiVersion}/short-urls"), headers: {
"X-Api-Key": _api_key ?? "",
});
if (response.statusCode == 200) {
return left("");
}
else {
try {
var jsonBody = jsonDecode(response.body);
return right(ApiFailure(jsonBody["type"], jsonBody["detail"], jsonBody["title"], jsonBody["status"]));
}
catch(resErr) {
return right(RequestFailure(response.statusCode, resErr.toString()));
}
}
}
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString()));
}
}
FutureOr<Either<List<ShortURL>, Failure>> getShortUrls() async {
var _currentPage = 1;
var _maxPages = 2;
List<ShortURL> _allUrls = [];
Failure? error;
while (_currentPage <= _maxPages) {
final response = await _getShortUrlPage(_currentPage);
response.fold((l) {
_allUrls.addAll(l.urls);
_maxPages = l.totalPages;
_currentPage++;
}, (r) {
_maxPages = 0;
error = r;
});
}
if (error == null) {
return left(_allUrls);
}
else {
return right(error!);
}
}
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(int page) async {
try {
final response = await http.get(Uri.parse("${_server_url}/rest/v${apiVersion}/short-urls?page=${page}"), headers: {
"X-Api-Key": _api_key ?? "",
});
if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body);
var pagesCount = jsonResponse["shortUrls"]["pagination"]["pagesCount"] as int;
List<ShortURL> shortURLs = (jsonResponse["shortUrls"]["data"] as List<dynamic>).map((e) {
return ShortURL.fromJson(e);
}).toList();
return left(ShortURLPageResponse(shortURLs, pagesCount));
}
else {
try {
var jsonBody = jsonDecode(response.body);
return right(ApiFailure(jsonBody["type"], jsonBody["detail"], jsonBody["title"], jsonBody["status"]));
}
catch(resErr) {
return right(RequestFailure(response.statusCode, resErr.toString()));
}
}
}
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString()));
}
}
}
class ShortURLPageResponse {
List<ShortURL> urls;
int totalPages;
ShortURLPageResponse(this.urls, this.totalPages);
}
abstract class Failure {}
class RequestFailure extends Failure {
int statusCode;
String description;
RequestFailure(this.statusCode, this.description);
}
class ApiFailure extends Failure {
String type;
String detail;
String title;
int status;
ApiFailure(this.type, this.detail, this.title, this.status);
}

38
lib/HomeView.dart Normal file
View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'globals.dart' as globals;
class HomeView extends StatefulWidget {
const HomeView({Key? key}) : super(key: key);
@override
State<HomeView> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
expandedHeight: 160,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shlink", style: TextStyle(fontWeight: FontWeight.bold)),
Text(globals.serverManager.getServerUrl(), style: TextStyle(fontSize: 16, color: Colors.grey[600]))
],
),
)
],
),
);
}
}

134
lib/LoginView.dart Normal file
View File

@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:shlink_app/API/ServerManager.dart';
import 'package:shlink_app/main.dart';
import 'globals.dart' as globals;
class LoginView extends StatefulWidget {
const LoginView({Key? key}) : super(key: key);
@override
State<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
late TextEditingController _server_url_controller;
late TextEditingController _apikey_controller;
bool _isLoggingIn = false;
String _errorMessage = "";
@override
void initState() {
// TODO: implement initState
super.initState();
_server_url_controller = TextEditingController();
_apikey_controller = TextEditingController();
}
void _connect() async {
setState(() {
_isLoggingIn = true;
_errorMessage = "";
});
final connectResult = await globals.serverManager.initAndConnect(_server_url_controller.text, _apikey_controller.text);
connectResult.fold((l) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const InitialPage())
);
setState(() {
_isLoggingIn = false;
});
}, (r) {
if (r is ApiFailure) {
setState(() {
_errorMessage = r.detail;
_isLoggingIn = false;
});
}
else if (r is RequestFailure) {
setState(() {
_errorMessage = r.description;
_isLoggingIn = false;
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
title: const Text("Add server")
),
SliverFillRemaining(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(padding: EdgeInsets.only(bottom: 8),
child: Text("Server URL", style: TextStyle(fontWeight: FontWeight.bold),)),
TextField(
controller: _server_url_controller,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "https://shlink.example.com"
),
),
const Padding(
padding: EdgeInsets.only(top: 8, bottom: 8),
child: Text("API Key", style: TextStyle(fontWeight: FontWeight.bold)),
),
TextField(
controller: _apikey_controller,
keyboardType: TextInputType.text,
obscureText: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "..."
),
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => {
_connect()
},
child: _isLoggingIn ? Container(
width: 34,
height: 34,
padding: const EdgeInsets.all(4),
child: const CircularProgressIndicator(),
) : const Text("Connect", style: TextStyle(fontSize: 20)),
)
],
),
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(child: Text(_errorMessage, style: TextStyle(color: Colors.red), textAlign: TextAlign.center))
],
),
)
],
),
)
)
],
)
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:shlink_app/HomeView.dart';
import 'package:shlink_app/URLListView.dart';
class NavigationBarView extends StatefulWidget {
const NavigationBarView({Key? key}) : super(key: key);
@override
State<NavigationBarView> createState() => _NavigationBarViewState();
}
class _NavigationBarViewState extends State<NavigationBarView> {
final List<Widget> views = [HomeView(), URLListView()];
int _selectedView = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: views.elementAt(_selectedView),
),
bottomNavigationBar: NavigationBar(
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: "Home"),
NavigationDestination(icon: Icon(Icons.link), label: "Short URLs")
],
selectedIndex: _selectedView,
onDestinationSelected: (int index) {
setState(() {
_selectedView = index;
});
},
),
);
}
}

97
lib/URLListView.dart Normal file
View File

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:shlink_app/API/Classes/ShortURL.dart';
import 'package:shlink_app/API/ServerManager.dart';
import 'globals.dart' as globals;
class URLListView extends StatefulWidget {
const URLListView({Key? key}) : super(key: key);
@override
State<URLListView> createState() => _URLListViewState();
}
class _URLListViewState extends State<URLListView> {
List<ShortURL> shortUrls = [];
@override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => loadAllShortUrls());
}
void loadAllShortUrls() async {
final response = await globals.serverManager.getShortUrls();
response.fold((l) {
setState(() {
shortUrls = l;
});
}, (r) {
var text = "";
if (r is RequestFailure) {
text = r.description;
}
else {
text = (r as ApiFailure).detail;
}
final snackBar = SnackBar(content: Text(text), behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
title: Text("All short URLs", style: TextStyle(fontWeight: FontWeight.bold))
),
SliverList(delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final shortURL = shortUrls[index];
return Padding(
padding: EdgeInsets.all(8),
child: Container(
padding: EdgeInsets.only(top: 8, bottom: 8),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)),
),
child: Padding(
padding: EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text("${shortURL.title ?? shortURL.shortCode}", textScaleFactor: 1.4, style: TextStyle(fontWeight: FontWeight.bold),),
Text("${shortURL.longUrl}",maxLines: 1, overflow: TextOverflow.ellipsis, textScaleFactor: 0.9, style: TextStyle(color: Colors.grey[600]),)
],
),
),
IconButton(onPressed: () {
}, icon: Icon(Icons.qr_code))
],
)
),
),
);
},
childCount: shortUrls.length
))
],
),
);
}
}

4
lib/globals.dart Normal file
View File

@ -0,0 +1,4 @@
library dev.abmgrt.shlink_app.globals;
import 'package:shlink_app/API/ServerManager.dart';
ServerManager serverManager = ServerManager();

79
lib/main.dart Normal file
View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shlink_app/LoginView.dart';
import 'package:shlink_app/NavigationBarView.dart';
import 'globals.dart' as globals;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
useMaterial3: true
),
darkTheme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.dark,
useMaterial3: true,
colorScheme: ColorScheme.dark(
background: Colors.black,
surface: Color(0xff0d0d0d),
secondaryContainer: Colors.grey[300]
)
),
themeMode: ThemeMode.system,
home: const InitialPage(),
);
}
}
class InitialPage extends StatefulWidget {
const InitialPage({super.key});
@override
State<InitialPage> createState() => _InitialPageState();
}
class _InitialPageState extends State<InitialPage> {
@override
void initState() {
super.initState();
checkLogin();
}
void checkLogin() async {
bool result = await globals.serverManager.checkLogin();
if (result) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const NavigationBarView())
);
}
else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const LoginView())
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text("")
),
);
}
}

298
pubspec.lock Normal file
View File

@ -0,0 +1,298 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
url: "https://pub.dev"
source: hosted
version: "2.10.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
url: "https://pub.dev"
source: hosted
version: "1.2.1"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
url: "https://pub.dev"
source: hosted
version: "1.17.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
url: "https://pub.dev"
source: hosted
version: "1.0.5"
dartz:
dependency: "direct main"
description:
name: dartz
sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168
url: "https://pub.dev"
source: hosted
version: "0.10.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_process_text:
dependency: "direct main"
description:
name: flutter_process_text
sha256: "75cdff9d255ce892c766370824e28bbb95a7f93e99b6c4109ca694a6ef4d5681"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b
url: "https://pub.dev"
source: hosted
version: "1.0.1"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: "direct main"
description:
name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
url: "https://pub.dev"
source: hosted
version: "0.13.6"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
js:
dependency: transitive
description:
name: js
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
url: "https://pub.dev"
source: hosted
version: "0.6.5"
lints:
dependency: transitive
description:
name: lints
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
url: "https://pub.dev"
source: hosted
version: "0.12.13"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.dev"
source: hosted
version: "0.2.0"
meta:
dependency: transitive
description:
name: meta
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
modal_bottom_sheet:
dependency: "direct main"
description:
name: modal_bottom_sheet
sha256: "3bba63c62d35c931bce7f8ae23a47f9a05836d8cb3c11122ada64e0b2f3d718f"
url: "https://pub.dev"
source: hosted
version: "3.0.0-pre"
path:
dependency: transitive
description:
name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
url: "https://pub.dev"
source: hosted
version: "1.8.2"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
url: "https://pub.dev"
source: hosted
version: "1.9.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
url: "https://pub.dev"
source: hosted
version: "1.11.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
url: "https://pub.dev"
source: hosted
version: "0.4.16"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
sdks:
dart: ">=2.19.6 <3.0.0"
flutter: ">=3.7.0"

95
pubspec.yaml Normal file
View File

@ -0,0 +1,95 @@
name: shlink_app
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=2.19.6 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
http: ^0.13.6
flutter_process_text: ^1.1.2
flutter_secure_storage: ^8.0.0
dartz: ^0.10.1
modal_bottom_sheet: ^3.0.0-pre
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

30
test/widget_test.dart Normal file
View File

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shlink_app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}