├── .gitignore
├── .metadata
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── io
│ │ │ │ └── flutter
│ │ │ │ └── app
│ │ │ │ └── FlutterMultiDexApplication.java
│ │ ├── kotlin
│ │ │ └── dev
│ │ │ │ └── abmgrt
│ │ │ │ └── shlink_app
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-hdpi
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── drawable-mdpi
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable-xhdpi
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── drawable-xxhdpi
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ └── ic_launcher.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
└── icon
│ ├── icon.png
│ ├── icon_background.png
│ ├── icon_foreground.png
│ └── icon_monochrome.png
├── lib
├── API
│ ├── Classes
│ │ ├── ShlinkStats
│ │ │ ├── shlink_stats.dart
│ │ │ └── shlink_stats_visits.dart
│ │ ├── ShortURL
│ │ │ ├── RedirectRule
│ │ │ │ ├── condition_device_type.dart
│ │ │ │ ├── redirect_rule.dart
│ │ │ │ ├── redirect_rule_condition.dart
│ │ │ │ └── redirect_rule_condition_type.dart
│ │ │ ├── short_url.dart
│ │ │ ├── short_url_meta.dart
│ │ │ └── visits_summary.dart
│ │ ├── ShortURLSubmission
│ │ │ └── short_url_submission.dart
│ │ └── Tag
│ │ │ └── tag_with_stats.dart
│ ├── Methods
│ │ ├── connect.dart
│ │ ├── delete_short_url.dart
│ │ ├── get_recent_short_urls.dart
│ │ ├── get_redirect_rules.dart
│ │ ├── get_server_health.dart
│ │ ├── get_shlink_stats.dart
│ │ ├── get_short_urls.dart
│ │ ├── get_tags_with_stats.dart
│ │ ├── set_redirect_rules.dart
│ │ ├── submit_short_url.dart
│ │ └── update_short_url.dart
│ └── server_manager.dart
├── global_theme.dart
├── globals.dart
├── main.dart
├── util
│ ├── build_api_error_snackbar.dart
│ ├── license.dart
│ └── string_to_color.dart
├── views
│ ├── home_view.dart
│ ├── login_view.dart
│ ├── navigationbar_view.dart
│ ├── opensource_licenses_view.dart
│ ├── redirect_rules_detail_view.dart
│ ├── settings_view.dart
│ ├── short_url_edit_view.dart
│ ├── tag_selector_view.dart
│ ├── url_detail_view.dart
│ └── url_list_view.dart
└── widgets
│ ├── available_servers_bottom_sheet.dart
│ └── url_tags_list_widget.dart
├── pubspec.lock
├── pubspec.yaml
└── test
└── widget_test.dart
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .packages
31 | .pub-cache/
32 | .pub/
33 | /build/
34 |
35 | # Symbolication related
36 | app.*.symbols
37 |
38 | # Obfuscation related
39 | app.*.map.json
40 |
41 | # Android Studio will place build artifacts here
42 | /android/app/debug
43 | /android/app/profile
44 | /android/app/release
45 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "2f708eb8396e362e280fac22cf171c2cb467343c"
8 | channel: "stable"
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
17 | base_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
18 | - platform: linux
19 | create_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
20 | base_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Adrian Baumgart
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 | # Shlink Manager
6 |
7 |
8 |
9 | Shlink Manager is an app for Android to see and manage all shortened URLs created with [Shlink](https://shlink.io/)
10 |
11 |
12 |
13 |
14 |
15 | [](https://codemagic.io/apps/66096ec96d57699debb805f8/66096ec96d57699debb805f7/latest_build)
16 |
17 | ## 📱 Current Features
18 |
19 | ✅ List all short URLs
20 | ✅ Create, edit and delete short URLs
21 | ✅ See overall statistics
22 | ✅ Detailed statistics for each short URL
23 | ✅ Display tags & QR code
24 | ✅ Dark mode support
25 | ✅ Quickly create Short URL via Share Sheet
26 | ✅ View rule-based redirects (no editing yet)
27 | ✅ Use multiple Shlink instances
28 |
29 | ## 🔨 To Do
30 | - [ ] Add support for iOS (maybe in the future)
31 | - [ ] improve app icon
32 | - [ ] Refactor code
33 | - [ ] ...and more
34 |
35 | ## 💻 Development
36 |
37 | First make sure you've installed [Flutter](https://flutter.dev/) and the necessary tools to build for Android/iOS (Android Studio/Xcode).
38 |
39 | ```bash
40 | $ git clone https://github.com/rainloreley/shlink-manager.git
41 | $ cd shlink-manager
42 | $ flutter pub get
43 | $ flutter run
44 | ```
45 |
46 | ## 📄 License
47 |
48 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
49 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 |
13 |
14 | linter:
15 | rules:
16 | # Style rules
17 | - camel_case_types
18 | - library_names
19 | - avoid_catching_errors
20 | - avoid_empty_else
21 | - unnecessary_brace_in_string_interps
22 | - avoid_redundant_argument_values
23 | - leading_newlines_in_multiline_strings
24 | # formatting
25 | - lines_longer_than_80_chars
26 | - curly_braces_in_flow_control_structures
27 | # doc comments
28 | - slash_for_doc_comments
29 | - package_api_docs
30 | # The lint rules applied to this project can be customized in the
31 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
32 | # included above or to enable additional rules. A list of all available lints
33 | # and their documentation is published at
34 | # https://dart-lang.github.io/linter/lints/index.html.
35 | #
36 | # Instead of disabling a lint rule for the entire project in the
37 | # section below, it can also be suppressed for a single line of code
38 | # or a specific dart file by using the `// ignore: name_of_lint` and
39 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
40 | # producing the lint.
41 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
42 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
43 |
44 | # Additional information about this file can be found at
45 | # https://dart.dev/guides/language/analysis-options
46 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 | def keystoreProperties = new Properties()
28 | def keystorePropertiesFile = rootProject.file('key.properties')
29 | if (keystorePropertiesFile.exists()) {
30 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
31 | }
32 |
33 | android {
34 | compileSdkVersion flutter.compileSdkVersion
35 | ndkVersion flutter.ndkVersion
36 |
37 | compileOptions {
38 | sourceCompatibility JavaVersion.VERSION_1_8
39 | targetCompatibility JavaVersion.VERSION_1_8
40 | }
41 |
42 | kotlinOptions {
43 | jvmTarget = '1.8'
44 | }
45 |
46 | sourceSets {
47 | main.java.srcDirs += 'src/main/kotlin'
48 | }
49 |
50 | defaultConfig {
51 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
52 | applicationId "dev.abmgrt.shlink_app"
53 | // You can update the following values to match your application needs.
54 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
55 | minSdkVersion flutter.minSdkVersion //flutter.minSdkVersion
56 | targetSdkVersion flutter.targetSdkVersion
57 | versionCode flutterVersionCode.toInteger()
58 | versionName flutterVersionName
59 |
60 | multiDexEnabled true
61 | }
62 |
63 | signingConfigs {
64 | release {
65 | keyAlias keystoreProperties['keyAlias']
66 | keyPassword keystoreProperties['keyPassword']
67 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
68 | storePassword keystoreProperties['storePassword']
69 | }
70 | }
71 | buildTypes {
72 | release {
73 | signingConfig signingConfigs.release
74 | }
75 | }
76 | }
77 |
78 | flutter {
79 | source '../..'
80 | }
81 |
82 | dependencies {
83 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
84 | implementation 'androidx.multidex:multidex:2.0.1'
85 | }
86 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
25 |
29 |
33 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
47 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java:
--------------------------------------------------------------------------------
1 | // Generated file.
2 | //
3 | // If you wish to remove Flutter's multidex support, delete this entire file.
4 | //
5 | // Modifications to this file should be done in a copy under a different name
6 | // as this file may be regenerated.
7 |
8 | package io.flutter.app;
9 |
10 | import android.app.Application;
11 | import android.content.Context;
12 | import androidx.annotation.CallSuper;
13 | import androidx.multidex.MultiDex;
14 |
15 | /**
16 | * Extension of {@link android.app.Application}, adding multidex support.
17 | */
18 | public class FlutterMultiDexApplication extends Application {
19 | @Override
20 | @CallSuper
21 | protected void attachBaseContext(Context base) {
22 | super.attachBaseContext(base);
23 | MultiDex.install(this);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/dev/abmgrt/shlink_app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.abmgrt.shlink_app
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ffffff
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.7.10'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:7.3.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | tasks.register("clean", Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/assets/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/assets/icon/icon.png
--------------------------------------------------------------------------------
/assets/icon/icon_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/assets/icon/icon_background.png
--------------------------------------------------------------------------------
/assets/icon/icon_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/assets/icon/icon_foreground.png
--------------------------------------------------------------------------------
/assets/icon/icon_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rainloreley/shlink-manager/d11d0cfe5c589ef0f395f3ee74dc42a187c1955e/assets/icon/icon_monochrome.png
--------------------------------------------------------------------------------
/lib/API/Classes/ShlinkStats/shlink_stats.dart:
--------------------------------------------------------------------------------
1 | import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
2 |
3 | /// Includes data about the statistics of a Shlink instance
4 | class ShlinkStats {
5 | /// Data about non-orphan visits
6 | VisitsSummary nonOrphanVisits;
7 |
8 | /// Data about orphan visits (without any valid slug assigned)
9 | VisitsSummary orphanVisits;
10 |
11 | /// Total count of all short URLs
12 | int shortUrlsCount;
13 |
14 | /// Total count all all tags
15 | int tagsCount;
16 |
17 | ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount,
18 | this.tagsCount);
19 | }
20 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShlinkStats/shlink_stats_visits.dart:
--------------------------------------------------------------------------------
1 | /// Visitor data
2 | class ShlinkStatsVisits {
3 | /// Count of URL visits
4 | int total;
5 |
6 | /// Count of URL visits from humans
7 | int nonBots;
8 |
9 | /// Count of URL visits from bots/crawlers
10 | int bots;
11 |
12 | ShlinkStatsVisits(this.total, this.nonBots, this.bots);
13 |
14 | /// Converts the JSON data from the API to an instance of [ShlinkStatsVisits]
15 | ShlinkStatsVisits.fromJson(Map json)
16 | : total = json["total"],
17 | nonBots = json["nonBots"],
18 | bots = json["bots"];
19 | }
20 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShortURL/RedirectRule/condition_device_type.dart:
--------------------------------------------------------------------------------
1 | enum ConditionDeviceType {
2 | IOS,
3 | ANDROID,
4 | DESKTOP;
5 |
6 | static ConditionDeviceType fromApi(String api) {
7 | switch (api) {
8 | case "ios":
9 | return ConditionDeviceType.IOS;
10 | case "android":
11 | return ConditionDeviceType.ANDROID;
12 | case "desktop":
13 | return ConditionDeviceType.DESKTOP;
14 | }
15 | throw ArgumentError("Invalid type $api");
16 | }
17 | }
18 |
19 | extension ConditionTypeExtension on ConditionDeviceType {
20 | String get api {
21 | switch (this) {
22 | case ConditionDeviceType.IOS:
23 | return "ios";
24 | case ConditionDeviceType.ANDROID:
25 | return "android";
26 | case ConditionDeviceType.DESKTOP:
27 | return "desktop";
28 | }
29 | }
30 |
31 | String get humanReadable {
32 | switch (this) {
33 | case ConditionDeviceType.IOS:
34 | return "iOS";
35 | case ConditionDeviceType.ANDROID:
36 | return "Android";
37 | case ConditionDeviceType.DESKTOP:
38 | return "Desktop";
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShortURL/RedirectRule/redirect_rule.dart:
--------------------------------------------------------------------------------
1 | import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition.dart';
2 |
3 | /// Single redirect rule for a short URL.
4 | class RedirectRule {
5 | String longUrl;
6 | int priority;
7 | List conditions;
8 |
9 | RedirectRule(this.longUrl, this.priority, this.conditions);
10 |
11 | RedirectRule.fromJson(Map json)
12 | : longUrl = json["longUrl"],
13 | priority = json["priority"],
14 | conditions = (json["conditions"] as List)
15 | .map((e) => RedirectRuleCondition.fromJson(e))
16 | .toList();
17 |
18 | Map toJson() {
19 | return {
20 | "longUrl": longUrl,
21 | "conditions": conditions.map((e) => e.toJson()).toList()
22 | };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShortURL/RedirectRule/redirect_rule_condition.dart:
--------------------------------------------------------------------------------
1 | import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition_type.dart';
2 |
3 | class RedirectRuleCondition {
4 | RedirectRuleConditionType type;
5 | String matchValue;
6 | String? matchKey;
7 |
8 | RedirectRuleCondition(String type, this.matchValue, this.matchKey)
9 | : type = RedirectRuleConditionType.fromApi(type);
10 |
11 | RedirectRuleCondition.fromJson(Map json)
12 | : type = RedirectRuleConditionType.fromApi(json["type"]),
13 | matchValue = json["matchValue"],
14 | matchKey = json["matchKey"];
15 |
16 | Map toJson() {
17 | return {"type": type.api, "matchValue": matchValue, "matchKey": matchKey};
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShortURL/RedirectRule/redirect_rule_condition_type.dart:
--------------------------------------------------------------------------------
1 | enum RedirectRuleConditionType {
2 | DEVICE,
3 | LANGUAGE,
4 | QUERY_PARAM;
5 |
6 | static RedirectRuleConditionType fromApi(String api) {
7 | switch (api) {
8 | case "device":
9 | return RedirectRuleConditionType.DEVICE;
10 | case "language":
11 | return RedirectRuleConditionType.LANGUAGE;
12 | case "query-param":
13 | return RedirectRuleConditionType.QUERY_PARAM;
14 | }
15 | throw ArgumentError("Invalid type $api");
16 | }
17 | }
18 |
19 | extension ConditionTypeExtension on RedirectRuleConditionType {
20 | String get api {
21 | switch (this) {
22 | case RedirectRuleConditionType.DEVICE:
23 | return "device";
24 | case RedirectRuleConditionType.LANGUAGE:
25 | return "language";
26 | case RedirectRuleConditionType.QUERY_PARAM:
27 | return "query-param";
28 | }
29 | }
30 |
31 | String get humanReadable {
32 | switch (this) {
33 | case RedirectRuleConditionType.DEVICE:
34 | return "Device";
35 | case RedirectRuleConditionType.LANGUAGE:
36 | return "Language";
37 | case RedirectRuleConditionType.QUERY_PARAM:
38 | return "Query parameter";
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShortURL/short_url.dart:
--------------------------------------------------------------------------------
1 | import 'package:shlink_app/API/Classes/ShortURL/short_url_meta.dart';
2 | import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
3 |
4 | import 'RedirectRule/redirect_rule.dart';
5 |
6 | /// Data about a short URL
7 | class ShortURL {
8 | /// Slug of the short URL used in the URL
9 | String shortCode;
10 |
11 | /// Entire short URL
12 | String shortUrl;
13 |
14 | /// Long URL where the user gets redirected to
15 | String longUrl;
16 |
17 | /// Creation date of the short URL
18 | DateTime dateCreated;
19 |
20 | /// Visitor data
21 | VisitsSummary visitsSummary;
22 |
23 | /// List of tags assigned to this short URL
24 | List tags;
25 |
26 | /// Metadata
27 | ShortURLMeta meta;
28 |
29 | /// Associated domain
30 | String? domain;
31 |
32 | /// Optional title
33 | String? title;
34 |
35 | /// Whether the short URL is crawlable by a web crawler
36 | bool crawlable;
37 |
38 | List? redirectRules;
39 |
40 | ShortURL(
41 | this.shortCode,
42 | this.shortUrl,
43 | this.longUrl,
44 | this.dateCreated,
45 | this.visitsSummary,
46 | this.tags,
47 | this.meta,
48 | this.domain,
49 | this.title,
50 | this.crawlable);
51 |
52 | /// Converts the JSON data from the API to an instance of [ShortURL]
53 | ShortURL.fromJson(Map json)
54 | : shortCode = json["shortCode"],
55 | shortUrl = json["shortUrl"],
56 | longUrl = json["longUrl"],
57 | dateCreated = DateTime.parse(json["dateCreated"]),
58 | visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]),
59 | tags =
60 | (json["tags"] as List).map((e) => e.toString()).toList(),
61 | meta = ShortURLMeta.fromJson(json["meta"]),
62 | domain = json["domain"],
63 | title = json["title"],
64 | crawlable = json["crawlable"];
65 |
66 | /// Returns an empty class of [ShortURL]
67 | ShortURL.empty()
68 | : shortCode = "",
69 | shortUrl = "",
70 | longUrl = "",
71 | dateCreated = DateTime.now(),
72 | visitsSummary = VisitsSummary(0, 0, 0),
73 | tags = [],
74 | meta = ShortURLMeta(DateTime.now(), DateTime.now(), 0),
75 | domain = "",
76 | title = "",
77 | crawlable = false;
78 | }
79 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShortURL/short_url_meta.dart:
--------------------------------------------------------------------------------
1 | /// Metadata for a short URL
2 | class ShortURLMeta {
3 | /// The date since when this short URL has been valid
4 | DateTime? validSince;
5 |
6 | /// The data when this short URL expires
7 | DateTime? validUntil;
8 |
9 | /// Amount of maximum visits allowed to this short URL
10 | int? maxVisits;
11 |
12 | ShortURLMeta(this.validSince, this.validUntil, this.maxVisits);
13 |
14 | /// Converts JSON data from the API to an instance of [ShortURLMeta]
15 | ShortURLMeta.fromJson(Map json)
16 | : validSince = json["validSince"] != null
17 | ? DateTime.parse(json["validSince"])
18 | : null,
19 | validUntil = json["validUntil"] != null
20 | ? DateTime.parse(json["validUntil"])
21 | : null,
22 | maxVisits = json["maxVisits"];
23 | }
24 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShortURL/visits_summary.dart:
--------------------------------------------------------------------------------
1 | /// Visitor data
2 | class VisitsSummary {
3 | /// Count of total visits
4 | int total;
5 |
6 | /// Count of visits from humans
7 | int nonBots;
8 |
9 | /// Count of visits from bots/crawlers
10 | int bots;
11 |
12 | VisitsSummary(this.total, this.nonBots, this.bots);
13 |
14 | /// Converts JSON data from the API to an instance of [VisitsSummary]
15 | VisitsSummary.fromJson(Map json)
16 | : total = json["total"] as int,
17 | nonBots = json["nonBots"] as int,
18 | bots = json["bots"] as int;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/API/Classes/ShortURLSubmission/short_url_submission.dart:
--------------------------------------------------------------------------------
1 | /// Data for a short URL which can be submitted to the server
2 | class ShortURLSubmission {
3 | /// Long URL to redirect to
4 | String longUrl;
5 |
6 | /// Date since when this short URL is valid in ISO8601 format
7 | String? validSince;
8 |
9 | /// Date until when this short URL is valid in ISO8601 format
10 | String? validUntil;
11 |
12 | /// Amount of maximum visits allowed to this short URLs
13 | int? maxVisits;
14 |
15 | /// List of tags assigned to this short URL
16 | List tags;
17 |
18 | /// Title of the page
19 | String? title;
20 |
21 | /// Whether the short URL is crawlable by web crawlers
22 | bool crawlable;
23 |
24 | /// Whether to forward query parameters
25 | bool forwardQuery;
26 |
27 | /// Custom slug (if not provided a random one will be generated)
28 | String? customSlug;
29 |
30 | /// Whether to use an existing short URL if the slug matches
31 | bool findIfExists;
32 |
33 | /// Domain to use
34 | String? domain;
35 |
36 | /// Length of the slug if a custom one is not provided
37 | int? shortCodeLength;
38 |
39 | ShortURLSubmission(
40 | {required this.longUrl,
41 | this.validSince,
42 | this.validUntil,
43 | this.maxVisits,
44 | required this.tags,
45 | this.title,
46 | required this.crawlable,
47 | required this.forwardQuery,
48 | this.customSlug,
49 | required this.findIfExists,
50 | this.domain,
51 | this.shortCodeLength});
52 |
53 | /// Converts class data to a JSON object
54 | Map toJson() {
55 | return {
56 | "longUrl": longUrl,
57 | "validSince": validSince,
58 | "validUntil": validUntil,
59 | "maxVisits": maxVisits,
60 | "tags": tags,
61 | "title": title,
62 | "crawlable": crawlable,
63 | "forwardQuery": forwardQuery,
64 | "customSlug": customSlug,
65 | "findIfExists": findIfExists,
66 | "domain": domain,
67 | "shortCodeLength": shortCodeLength
68 | };
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/API/Classes/Tag/tag_with_stats.dart:
--------------------------------------------------------------------------------
1 | import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
2 |
3 | /// Tag with stats data
4 | class TagWithStats {
5 | /// Tag name
6 | String tag;
7 |
8 | /// Amount of short URLs using this tag
9 | int shortUrlsCount;
10 |
11 | /// visits summary for tag
12 | VisitsSummary visitsSummary;
13 |
14 | TagWithStats(this.tag, this.shortUrlsCount, this.visitsSummary);
15 |
16 | TagWithStats.fromJson(Map json)
17 | : tag = json["tag"] as String,
18 | shortUrlsCount = json["shortUrlsCount"] as int,
19 | visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]);
20 | }
--------------------------------------------------------------------------------
/lib/API/Methods/connect.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import '../server_manager.dart';
6 |
7 | /// Tries to connect to the Shlink server
8 | FutureOr> apiConnect(
9 | String? apiKey, String? serverUrl, String apiVersion) async {
10 | try {
11 | final response = await http
12 | .get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
13 | "X-Api-Key": apiKey ?? "",
14 | });
15 | if (response.statusCode == 200) {
16 | return left("");
17 | } else {
18 | try {
19 | var jsonBody = jsonDecode(response.body);
20 | return right(ApiFailure(
21 | type: jsonBody["type"],
22 | detail: jsonBody["detail"],
23 | title: jsonBody["title"],
24 | status: jsonBody["status"]));
25 | } catch (resErr) {
26 | return right(RequestFailure(response.statusCode, resErr.toString()));
27 | }
28 | }
29 | } catch (reqErr) {
30 | return right(RequestFailure(0, reqErr.toString()));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/API/Methods/delete_short_url.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import '../server_manager.dart';
6 |
7 | /// Deletes a short URL from the server
8 | FutureOr> apiDeleteShortUrl(String shortCode,
9 | String? apiKey, String? serverUrl, String apiVersion) async {
10 | try {
11 | final response = await http.delete(
12 | Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"),
13 | headers: {
14 | "X-Api-Key": apiKey ?? "",
15 | });
16 | if (response.statusCode == 204) {
17 | // get returned short url
18 | return left("");
19 | } else {
20 | try {
21 | var jsonBody = jsonDecode(response.body);
22 | return right(ApiFailure(
23 | type: jsonBody["type"],
24 | detail: jsonBody["detail"],
25 | title: jsonBody["title"],
26 | status: jsonBody["status"]));
27 | } catch (resErr) {
28 | return right(RequestFailure(response.statusCode, resErr.toString()));
29 | }
30 | }
31 | } catch (reqErr) {
32 | return right(RequestFailure(0, reqErr.toString()));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/API/Methods/get_recent_short_urls.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
6 | import '../server_manager.dart';
7 |
8 | /// Gets recently created short URLs from the server
9 | FutureOr, Failure>> apiGetRecentShortUrls(
10 | String? apiKey, String? serverUrl, String apiVersion) async {
11 | try {
12 | final response = await http.get(
13 | Uri.parse(
14 | "$serverUrl/rest/v$apiVersion/short-urls?itemsPerPage=5&orderBy=dateCreated-DESC"),
15 | headers: {
16 | "X-Api-Key": apiKey ?? "",
17 | });
18 | if (response.statusCode == 200) {
19 | var jsonResponse = jsonDecode(response.body);
20 | List shortURLs =
21 | (jsonResponse["shortUrls"]["data"] as List).map((e) {
22 | return ShortURL.fromJson(e);
23 | }).toList();
24 | return left(shortURLs);
25 | } else {
26 | try {
27 | var jsonBody = jsonDecode(response.body);
28 | return right(ApiFailure(
29 | type: jsonBody["type"],
30 | detail: jsonBody["detail"],
31 | title: jsonBody["title"],
32 | status: jsonBody["status"]));
33 | } catch (resErr) {
34 | return right(RequestFailure(response.statusCode, resErr.toString()));
35 | }
36 | }
37 | } catch (reqErr) {
38 | return right(RequestFailure(0, reqErr.toString()));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/API/Methods/get_redirect_rules.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule.dart';
6 | import '../server_manager.dart';
7 |
8 | /// Gets redirect rules for a given short URL (code).
9 | FutureOr, Failure>> apiGetRedirectRules(
10 | String shortCode,
11 | String? apiKey,
12 | String? serverUrl,
13 | String apiVersion) async {
14 | try {
15 | final response = await http.get(
16 | Uri.parse(
17 | "$serverUrl/rest/v$apiVersion/short-urls/$shortCode/redirect-rules"),
18 | headers: {
19 | "X-Api-Key": apiKey ?? "",
20 | });
21 | if (response.statusCode == 200) {
22 | // get returned redirect rules
23 | var jsonBody = jsonDecode(response.body) as Map;
24 |
25 | // convert json array to object array
26 | List redirectRules =
27 | (jsonBody["redirectRules"] as List)
28 | .map((e) => RedirectRule.fromJson(e))
29 | .toList();
30 |
31 | return left(redirectRules);
32 | } else {
33 | try {
34 | var jsonBody = jsonDecode(response.body);
35 | return right(ApiFailure(
36 | type: jsonBody["type"],
37 | detail: jsonBody["detail"],
38 | title: jsonBody["title"],
39 | status: jsonBody["status"],
40 | invalidElements: jsonBody["invalidElements"]));
41 | } catch (resErr) {
42 | return right(RequestFailure(response.statusCode, resErr.toString()));
43 | }
44 | }
45 | } catch (reqErr) {
46 | return right(RequestFailure(0, reqErr.toString()));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/API/Methods/get_server_health.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import '../server_manager.dart';
6 |
7 | /// Gets the status of the server and health information
8 | FutureOr> apiGetServerHealth(
9 | String? apiKey, String? serverUrl, String apiVersion) async {
10 | try {
11 | final response = await http
12 | .get(Uri.parse("$serverUrl/rest/v$apiVersion/health"), headers: {
13 | "X-Api-Key": apiKey ?? "",
14 | });
15 | if (response.statusCode == 200) {
16 | var jsonData = jsonDecode(response.body);
17 | return left(ServerHealthResponse(
18 | status: jsonData["status"], version: jsonData["version"]));
19 | } else {
20 | try {
21 | var jsonBody = jsonDecode(response.body);
22 | return right(ApiFailure(
23 | type: jsonBody["type"],
24 | detail: jsonBody["detail"],
25 | title: jsonBody["title"],
26 | status: jsonBody["status"]));
27 | } catch (resErr) {
28 | return right(RequestFailure(response.statusCode, resErr.toString()));
29 | }
30 | }
31 | } catch (reqErr) {
32 | return right(RequestFailure(0, reqErr.toString()));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/API/Methods/get_shlink_stats.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
6 | import '../Classes/ShlinkStats/shlink_stats.dart';
7 | import '../server_manager.dart';
8 |
9 | /// Gets statistics about the Shlink server
10 | FutureOr> apiGetShlinkStats(
11 | String? apiKey, String? serverUrl, String apiVersion) async {
12 | VisitsSummary? nonOrphanVisits;
13 | VisitsSummary? orphanVisits;
14 | int shortUrlsCount = 0;
15 | int tagsCount = 0;
16 | Failure? failure;
17 |
18 | var visitStatsResponse = await _getVisitStats(apiKey, serverUrl, apiVersion);
19 | visitStatsResponse.fold((l) {
20 | nonOrphanVisits = l.nonOrphanVisits;
21 | orphanVisits = l.orphanVisits;
22 | }, (r) {
23 | failure = r;
24 | return right(r);
25 | });
26 |
27 | var shortUrlsCountResponse =
28 | await _getShortUrlsCount(apiKey, serverUrl, apiVersion);
29 | shortUrlsCountResponse.fold((l) {
30 | shortUrlsCount = l;
31 | }, (r) {
32 | failure = r;
33 | return right(r);
34 | });
35 |
36 | var tagsCountResponse = await _getTagsCount(apiKey, serverUrl, apiVersion);
37 | tagsCountResponse.fold((l) {
38 | tagsCount = l;
39 | }, (r) {
40 | failure = r;
41 | return right(r);
42 | });
43 |
44 | while (failure == null && (orphanVisits == null)) {
45 | await Future.delayed(const Duration(milliseconds: 100));
46 | }
47 |
48 | if (failure != null) {
49 | return right(failure!);
50 | }
51 | return left(
52 | ShlinkStats(nonOrphanVisits!, orphanVisits!, shortUrlsCount, tagsCount));
53 | }
54 |
55 | class _ShlinkVisitStats {
56 | VisitsSummary nonOrphanVisits;
57 | VisitsSummary orphanVisits;
58 |
59 | _ShlinkVisitStats(this.nonOrphanVisits, this.orphanVisits);
60 | }
61 |
62 | /// Gets visitor statistics about the entire server
63 | FutureOr> _getVisitStats(
64 | String? apiKey, String? serverUrl, String apiVersion) async {
65 | try {
66 | final response = await http
67 | .get(Uri.parse("$serverUrl/rest/v$apiVersion/visits"), headers: {
68 | "X-Api-Key": apiKey ?? "",
69 | });
70 | if (response.statusCode == 200) {
71 | var jsonResponse = jsonDecode(response.body);
72 | var nonOrphanVisits =
73 | VisitsSummary.fromJson(jsonResponse["visits"]["nonOrphanVisits"]);
74 | var orphanVisits =
75 | VisitsSummary.fromJson(jsonResponse["visits"]["orphanVisits"]);
76 | return left(_ShlinkVisitStats(nonOrphanVisits, orphanVisits));
77 | } else {
78 | try {
79 | var jsonBody = jsonDecode(response.body);
80 | return right(ApiFailure(
81 | type: jsonBody["type"],
82 | detail: jsonBody["detail"],
83 | title: jsonBody["title"],
84 | status: jsonBody["status"]));
85 | } catch (resErr) {
86 | return right(RequestFailure(response.statusCode, resErr.toString()));
87 | }
88 | }
89 | } catch (reqErr) {
90 | return right(RequestFailure(0, reqErr.toString()));
91 | }
92 | }
93 |
94 | /// Gets amount of short URLs
95 | FutureOr> _getShortUrlsCount(
96 | String? apiKey, String? serverUrl, String apiVersion) async {
97 | try {
98 | final response = await http
99 | .get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
100 | "X-Api-Key": apiKey ?? "",
101 | });
102 | if (response.statusCode == 200) {
103 | var jsonResponse = jsonDecode(response.body);
104 | return left(jsonResponse["shortUrls"]["pagination"]["totalItems"]);
105 | } else {
106 | try {
107 | var jsonBody = jsonDecode(response.body);
108 | return right(ApiFailure(
109 | type: jsonBody["type"],
110 | detail: jsonBody["detail"],
111 | title: jsonBody["title"],
112 | status: jsonBody["status"]));
113 | } catch (resErr) {
114 | return right(RequestFailure(response.statusCode, resErr.toString()));
115 | }
116 | }
117 | } catch (reqErr) {
118 | return right(RequestFailure(0, reqErr.toString()));
119 | }
120 | }
121 |
122 | /// Gets amount of tags
123 | FutureOr> _getTagsCount(
124 | String? apiKey, String? serverUrl, String apiVersion) async {
125 | try {
126 | final response = await http
127 | .get(Uri.parse("$serverUrl/rest/v$apiVersion/tags"), headers: {
128 | "X-Api-Key": apiKey ?? "",
129 | });
130 | if (response.statusCode == 200) {
131 | var jsonResponse = jsonDecode(response.body);
132 | return left(jsonResponse["tags"]["pagination"]["totalItems"]);
133 | } else {
134 | try {
135 | var jsonBody = jsonDecode(response.body);
136 | return right(ApiFailure(
137 | type: jsonBody["type"],
138 | detail: jsonBody["detail"],
139 | title: jsonBody["title"],
140 | status: jsonBody["status"]));
141 | } catch (resErr) {
142 | return right(RequestFailure(response.statusCode, resErr.toString()));
143 | }
144 | }
145 | } catch (reqErr) {
146 | return right(RequestFailure(0, reqErr.toString()));
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/lib/API/Methods/get_short_urls.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
6 | import '../server_manager.dart';
7 |
8 | /// Gets all short URLs
9 | FutureOr, Failure>> apiGetShortUrls(
10 | String? apiKey, String? serverUrl, String apiVersion) async {
11 | var currentPage = 1;
12 | var maxPages = 2;
13 | List allUrls = [];
14 |
15 | Failure? error;
16 |
17 | while (currentPage <= maxPages) {
18 | final response =
19 | await _getShortUrlPage(currentPage, apiKey, serverUrl, apiVersion);
20 | response.fold((l) {
21 | allUrls.addAll(l.urls);
22 | maxPages = l.totalPages;
23 | currentPage++;
24 | }, (r) {
25 | maxPages = 0;
26 | error = r;
27 | });
28 | }
29 | if (error == null) {
30 | return left(allUrls);
31 | } else {
32 | return right(error!);
33 | }
34 | }
35 |
36 | /// Gets all short URLs from a specific page
37 | FutureOr> _getShortUrlPage(
38 | int page, String? apiKey, String? serverUrl, String apiVersion) async {
39 | try {
40 | final response = await http.get(
41 | Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?page=$page"),
42 | headers: {
43 | "X-Api-Key": apiKey ?? "",
44 | });
45 | if (response.statusCode == 200) {
46 | var jsonResponse = jsonDecode(response.body);
47 | var pagesCount =
48 | jsonResponse["shortUrls"]["pagination"]["pagesCount"] as int;
49 | List shortURLs =
50 | (jsonResponse["shortUrls"]["data"] as List).map((e) {
51 | return ShortURL.fromJson(e);
52 | }).toList();
53 | return left(ShortURLPageResponse(shortURLs, pagesCount));
54 | } else {
55 | try {
56 | var jsonBody = jsonDecode(response.body);
57 | return right(ApiFailure(
58 | type: jsonBody["type"],
59 | detail: jsonBody["detail"],
60 | title: jsonBody["title"],
61 | status: jsonBody["status"]));
62 | } catch (resErr) {
63 | return right(RequestFailure(response.statusCode, resErr.toString()));
64 | }
65 | }
66 | } catch (reqErr) {
67 | return right(RequestFailure(0, reqErr.toString()));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/API/Methods/get_tags_with_stats.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:shlink_app/API/Classes/Tag/tag_with_stats.dart';
6 | import '../server_manager.dart';
7 |
8 | /// Gets all tags
9 | FutureOr, Failure>> apiGetTagsWithStats(
10 | String? apiKey, String? serverUrl, String apiVersion) async {
11 | var currentPage = 1;
12 | var maxPages = 2;
13 | List allTags = [];
14 |
15 | Failure? error;
16 |
17 | while (currentPage <= maxPages) {
18 | final response =
19 | await _getTagsWithStatsPage(currentPage, apiKey, serverUrl, apiVersion);
20 | response.fold((l) {
21 | allTags.addAll(l.tags);
22 | maxPages = l.totalPages;
23 | currentPage++;
24 | }, (r) {
25 | maxPages = 0;
26 | error = r;
27 | });
28 | }
29 | if (error == null) {
30 | return left(allTags);
31 | } else {
32 | return right(error!);
33 | }
34 | }
35 |
36 | /// Gets all tags from a specific page
37 | FutureOr> _getTagsWithStatsPage(
38 | int page, String? apiKey, String? serverUrl, String apiVersion) async {
39 | try {
40 | final response = await http.get(
41 | Uri.parse("$serverUrl/rest/v$apiVersion/tags/stats?page=$page"),
42 | headers: {
43 | "X-Api-Key": apiKey ?? "",
44 | });
45 | if (response.statusCode == 200) {
46 | var jsonResponse = jsonDecode(response.body);
47 | var pagesCount = jsonResponse["tags"]["pagination"]["pagesCount"] as int;
48 | List tags =
49 | (jsonResponse["tags"]["data"] as List).map((e) {
50 | return TagWithStats.fromJson(e);
51 | }).toList();
52 | return left(TagsWithStatsPageResponse(tags, pagesCount));
53 | } else {
54 | try {
55 | var jsonBody = jsonDecode(response.body);
56 | return right(ApiFailure(
57 | type: jsonBody["type"],
58 | detail: jsonBody["detail"],
59 | title: jsonBody["title"],
60 | status: jsonBody["status"]));
61 | } catch (resErr) {
62 | return right(RequestFailure(response.statusCode, resErr.toString()));
63 | }
64 | }
65 | } catch (reqErr) {
66 | return right(RequestFailure(0, reqErr.toString()));
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/API/Methods/set_redirect_rules.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule.dart';
6 | import '../server_manager.dart';
7 |
8 | /// Saves the redirect rules for a given short URL (code).
9 | FutureOr> apiSetRedirectRules(
10 | String shortCode,
11 | List redirectRules,
12 | String? apiKey,
13 | String? serverUrl,
14 | String apiVersion) async {
15 | try {
16 | Map body = {};
17 | List