56 Commits

Author SHA1 Message Date
MineTec 3b1b0d0c19 fixed lesson merging mutation, improved overlap detection, and implemented priority-based lane assignment with tablet support 2026-05-07 13:27:40 +02:00
MineTec c32e64fe74 improved yOfDateTime precision and period-based calculation in workweek calendar 2026-05-07 09:51:13 +02:00
MineTec 710e88d744 refactored chat data fetching to support separate cache and network callbacks 2026-05-07 09:46:30 +02:00
MineTec 517e515ac1 centered login view and updated layout constraints 2026-05-07 09:11:09 +02:00
MineTec e8f0c4383c added camera support and enabled gallery selection on ios 2026-05-06 23:09:44 +02:00
MineTec b8cac73e74 updated timetable UI with event status and enhanced appointment tile rendering 2026-05-06 22:53:24 +02:00
MineTec 95ef29fb09 implemented dynamic module settings and configurable bottom bar, added all-day event support to timetable, and overhauled marianum dates UI with month grouping and search 2026-05-06 22:37:41 +02:00
MineTec 86d12884fc custom login implementation, period-based timetable layout with overlap handling, enhanced error dialogs, and unified bottom sheets 2026-05-06 20:42:09 +02:00
MineTec 50d2941e52 refactored lesson details, centralized logout logic, and added resume re-fetch 2026-05-06 16:27:45 +02:00
MineTec 71506aab2d Merge remote-tracking branch 'origin/develop-refactor' into develop-refactor 2026-05-06 11:59:17 +02:00
MineTec 4e1272aba9 claude refactorings, flutter best practices, platform dependent changes, general cleanup 2026-05-06 11:59:01 +02:00
MineTec 72ebe6f7e7 claude refactorings, flutter best practices, platform dependent changes, general cleanup 2026-05-06 11:58:50 +02:00
MineTec 4b1d4379a0 loading state and error handling refactor 2026-05-06 10:11:45 +02:00
MineTec 2c376afd91 removed stray character in settings.gradle 2026-05-05 22:38:07 +02:00
MineTec 54ba04a7bd wait for account data population and set initial AccountBloc status 2026-05-05 22:08:10 +02:00
MineTec 9b5a70b285 api and storage restructure 2026-05-05 22:00:07 +02:00
MineTec 4f796dac2e folder restructuring 2026-05-05 21:44:23 +02:00
MineTec db9c3386f1 better loading indicators for timetables, talk and files 2026-05-05 21:07:48 +02:00
MineTec bee5c02a4f marianum appointments 2026-05-05 16:05:07 +02:00
MineTec e8faa77e70 refactored timetable 2026-05-05 13:49:45 +02:00
MineTec 551c1bf1fa claude refactor 2026-05-04 13:54:39 +02:00
MineTec 9973f12733 Merge pull request 'added opacity to past custom events' (#94) from develop-customEventOpacity into develop
Reviewed-on: #94
2026-05-04 10:20:05 +00:00
MineTec 278fed52f1 fixed indention 2026-05-04 12:19:43 +02:00
Pupsi f89ac87c51 added opacity to past custom events 2026-05-04 09:45:24 +02:00
MineTec 0b8380eff0 removed toolchain vendor restriction 2026-05-04 09:04:54 +02:00
Pupsi 4ef3a167ea fixed dart errors in notificationService 2026-05-04 08:51:16 +02:00
MineTec c26cefc073 moved native splash dependency to runtime dependencies 2026-04-04 00:38:25 +02:00
MineTec e78ef0df49 toolchain migration, agp upgrade, flutter upgrade, dependency upgrades 2026-04-04 00:30:24 +02:00
MineTec 0b6c80e84c fixed hanging chat when it contains any 'call' 2026-04-04 00:11:44 +02:00
Pupsi a52817231e fixed list view breaking layout 2026-02-01 17:16:42 +01:00
Pupsi f6933b6529 Merge pull request 'develop-polls' (#93) from develop-polls into develop
Reviewed-on: #93
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-02-01 14:56:23 +00:00
Pupsi e4243e53ac resolved pr issues 2026-02-01 15:55:43 +01:00
Pupsi 0aead45191 Merge remote-tracking branch 'origin/develop' into develop-polls
# Conflicts:
#	devtools_options.yaml
2026-02-01 15:37:26 +01:00
Pupsi dacefd321b updated poll list design 2026-02-01 15:35:40 +01:00
Pupsi 92a9a7358e changed link to directly open the chat 2026-02-01 15:20:01 +01:00
Pupsi 174e6ac0b7 fixed finished polls causing errors, made poll list dense 2026-02-01 15:07:48 +01:00
MineTec c9eaed782a update grade averages UI and enable devtools extensions 2026-02-01 15:06:49 +01:00
Pupsi 567184bcf9 filtered system messages for poll votes 2026-02-01 13:56:39 +01:00
Pupsi 541d6ef164 fixed issues with null values in votes map 2026-02-01 13:32:18 +01:00
Pupsi 3469d02033 changed poll dialog to only show results 2026-02-01 03:23:36 +01:00
Pupsi 699aec8ab5 Merge branch 'develop' into develop-polls 2026-02-01 00:06:51 +01:00
Pupsi 7a3a022ecd Merge remote-tracking branch 'origin/develop' into develop 2026-01-31 23:41:05 +01:00
Pupsi 9d8a99df7c added screen_brightness 2026-01-31 23:33:31 +01:00
Pupsi a47e52e8e7 added screen_brightness 2026-01-31 23:31:53 +01:00
MineTec a1fd21de04 Merge pull request 'develop-fileDownload' (#92) from develop-fileDownload into develop
Reviewed-on: #92
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-01-31 22:15:50 +00:00
Pupsi b3d8586c04 Merge remote-tracking branch 'origin/develop' into develop-fileDownload
# Conflicts:
#	pubspec.yaml
2026-01-31 22:59:46 +01:00
Pupsi 0409c5463f combined actions to popup menu 2026-01-31 22:55:49 +01:00
MineTec 7a3b69fade update dependencies and bump version to 0.1.7+45
- Bump version to `0.1.7+45` and update SDK constraint to `>=3.8.0 <4.0.0`.
- Update numerous dependencies
2026-01-31 22:40:50 +01:00
Pupsi df275c0108 added file saver 2026-01-31 22:17:31 +01:00
MineTec 0525453d48 migrate launcher icons configuration to standalone file 2026-01-31 21:09:56 +01:00
MineTec 4e8b2f34f9 bumped version 2026-01-26 17:13:58 +01:00
MineTec bc6a069c90 fixed voice-message preventing chat loading 2026-01-26 16:44:39 +01:00
MineTec bfa0b0f5c0 feat: add devtools extensions and fix poll dialog UI/UX
- Enabled `provider` and `shared_preferences` extensions in `devtools_options.yaml`.
- Added logging for message object data on chat bubble tap.
- Fixed layout issues in poll dialog by wrapping `LoadingSpinner` in a `Column` and changing `ListView` to a `Column` in `pollOptionsList.dart`.
- Updated poll submission button to wait for the poll state to load before allowing interaction.
2026-01-18 10:28:17 +01:00
MineTec 274b77f705 Merge branch 'develop' into develop-polls 2026-01-17 23:22:52 +01:00
Pupsi b68bec9ebd WIP: add option to vote on polls 2025-10-10 11:39:57 +02:00
Pupsi 81f65750b7 added functionality to show own votes in polls 2025-10-10 02:01:43 +02:00
494 changed files with 18918 additions and 9852 deletions
+73 -29
View File
@@ -1,44 +1,88 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
# Static analysis configuration for the Flutter project.
# https://dart.dev/guides/language/analysis-options
#
# 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`.
# Base ruleset: flutter_lints (recommended Flutter defaults).
# Additional lints below catch real bugs and enforce consistent style.
# 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
analyzer:
language:
strict-casts: true
strict-raw-types: true
errors:
invalid_annotation_target: ignore
todo: ignore
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "lib/firebase_options.dart"
- "build/**"
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
file_names: false
# === Project conventions ===
prefer_relative_imports: true
unnecessary_lambdas: true
prefer_single_quotes: true
prefer_if_elements_to_conditional_expressions: true
prefer_expression_function_bodies: true
omit_local_variable_types: true
eol_at_end_of_file: true
cast_nullable_to_non_nullable: true
avoid_void_async: true
omit_local_variable_types: true
avoid_multiple_declarations_per_line: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
# === Bug catchers ===
always_declare_return_types: true
avoid_empty_else: true
avoid_slow_async_io: true
avoid_type_to_string: true
avoid_void_async: true
await_only_futures: true
cancel_subscriptions: true
cast_nullable_to_non_nullable: true
close_sinks: true
empty_catches: true
hash_and_equals: true
no_adjacent_strings_in_list: true
no_duplicate_case_values: true
test_types_in_equals: true
throw_in_finally: true
unawaited_futures: true
unnecessary_statements: true
unrelated_type_equality_checks: true
use_build_context_synchronously: true
valid_regexps: true
# === Flutter widget hygiene ===
avoid_unnecessary_containers: true
sized_box_for_whitespace: true
sort_child_properties_last: true
use_colored_box: true
use_decorated_box: true
use_full_hex_values_for_flutter_colors: true
use_key_in_widget_constructors: true
# === Code clarity ===
directives_ordering: true
library_prefixes: true
no_leading_underscores_for_local_identifiers: true
prefer_conditional_assignment: true
prefer_if_elements_to_conditional_expressions: true
prefer_if_null_operators: true
prefer_initializing_formals: true
prefer_interpolation_to_compose_strings: true
prefer_is_empty: true
prefer_is_not_empty: true
prefer_is_not_operator: true
prefer_iterable_whereType: true
prefer_null_aware_operators: true
prefer_spread_collections: true
prefer_void_to_null: true
unnecessary_await_in_return: true
unnecessary_brace_in_string_interps: true
unnecessary_lambdas: true
unnecessary_null_aware_assignments: true
unnecessary_null_checks: true
unnecessary_parenthesis: true
unnecessary_string_interpolations: true
use_super_parameters: true
# === File naming ===
file_names: true
+1 -4
View File
@@ -25,7 +25,7 @@ if (flutterVersionName == null) {
android {
namespace "eu.mhsl.marianum.mobile.client"
compileSdk flutter.compileSdkVersion
ndkVersion "27.0.12077973"
ndkVersion "28.2.13676358"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -57,9 +57,6 @@ android {
signingConfig signingConfigs.debug
}
}
buildFeatures {
viewBinding true
}
}
flutter {
+27 -55
View File
@@ -1,73 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<!--
Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin.
-->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET" />
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
tools:replace="android:label"
android:label="Marianum Fulda"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:label="Marianum Fulda">
<receiver
android:name=".TimetableWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/timetable_widget_info" />
</receiver>
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="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
<!-- 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.
-->
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
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" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!--
Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
-->
<!-- 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>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -1,39 +0,0 @@
package eu.mhsl.marianum.mobile.client
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.RemoteViews
import es.antonborri.home_widget.HomeWidgetPlugin
import android.util.Base64
/**
* Implementation of App Widget functionality.
*/
class TimetableWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
for (appWidgetId in appWidgetIds) {
val widgetData = HomeWidgetPlugin.getData(context)
val views = RemoteViews(context.packageName, R.layout.timetable_widget).apply {
val imageBase64 = widgetData.getString("screen", null) ?: return@apply
val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT);
val imageBitmap: Bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
setImageViewBitmap(R.id.widget_image, imageBitmap)
}
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
val pendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.background, pendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Background for widgets to make the rounded corners based on the
appWidgetRadius attribute value
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="?attr/appWidgetRadius" />
<solid android:color="?android:attr/colorBackground" />
</shape>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Background for views inside widgets to make the rounded corners based on the
appWidgetInnerRadius attribute value
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="?attr/appWidgetInnerRadius" />
<solid android:color="?android:attr/colorAccent" />
</shape>
@@ -1,26 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/background"
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:padding="0dp"
android:theme="@style/Theme.Android.AppWidgetContainer">
<ImageView
android:id="@+id/widget_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_marginLeft="0dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
android:layout_weight="1"
android:adjustViewBounds="false"
android:background="@android:color/transparent"
android:scaleType="fitCenter"
android:src="@drawable/timetable_widget_default"
android:visibility="visible"
tools:visibility="visible" />
</RelativeLayout>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
-->
<style name="Theme.Android.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>
</resources>
@@ -1,14 +0,0 @@
<resources>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_inner_view_background</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
</resources>
@@ -18,18 +18,4 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item>
<item name="android:clipToOutline">true</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_inner_view_background</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:clipToOutline">true</item>
</style>
</resources>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius
and @android:dimen/system_app_widget_internal_padding requires API level 31
-->
<style name="Theme.Android.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>
</resources>
@@ -1,7 +0,0 @@
<resources>
<declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetPadding" format="dimension" />
<attr name="appWidgetInnerRadius" format="dimension" />
<attr name="appWidgetRadius" format="dimension" />
</declare-styleable>
</resources>
@@ -1,6 +0,0 @@
<resources>
<color name="light_blue_50">#FFE1F5FE</color>
<color name="light_blue_200">#FF81D4FA</color>
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
</resources>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Refer to App Widget Documentation for margin information
http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
-->
<dimen name="widget_margin">0dp</dimen>
</resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="appwidget_text">Marianum Vertretungsplan</string>
<string name="add_widget">Hinzufügen</string>
<string name="app_widget_description">Übersicht zum Vertretungsplan</string>
</resources>
@@ -19,14 +19,4 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:background">?android:attr/colorBackground</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:background">?android:attr/colorBackground</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
</resources>
@@ -1,17 +0,0 @@
<resources>
<style name="Theme.Android.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<!-- Radius of the outer bound of widgets to make the rounded corners -->
<item name="appWidgetRadius">16dp</item>
<!--
Radius of the inner view's bound of widgets to make the rounded corners.
It needs to be 8dp or less than the value of appWidgetRadius
-->
<item name="appWidgetInnerRadius">8dp</item>
</style>
<style name="Theme.Android.AppWidgetContainer" parent="Theme.Android.AppWidgetContainerParent">
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
<item name="appWidgetPadding">16dp</item>
</style>
</resources>
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_widget_description"
android:initialKeyguardLayout="@layout/timetable_widget"
android:initialLayout="@layout/timetable_widget"
android:minWidth="220dp"
android:minHeight="294dp"
android:minResizeWidth="110dp"
android:minResizeHeight="147dp"
android:previewImage="@drawable/timetable_widget_preview"
android:previewLayout="@layout/timetable_widget"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="4"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />
-15
View File
@@ -2,21 +2,6 @@ allprojects {
repositories {
google()
mavenCentral()
// [required] background_fetch
maven {
url "${project(':background_fetch').projectDir}/libs"
}
}
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10'
}
}
@@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=21
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+3 -1
View File
@@ -19,8 +19,10 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.7.3' apply false
id "com.android.application" version '8.13.2' apply false
id "com.android.library" version '8.13.2' apply false
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0'
}
include ":app"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+3
View File
@@ -1 +1,4 @@
extensions:
- hive_ce: true
- shared_preferences: true
- provider: true
+9
View File
@@ -0,0 +1,9 @@
flutter_launcher_icons:
android: true
ios: true
remove_alpha_ios: true
image_path_android: "assets/logo/icon/ic_launcher.png"
image_path_ios: "assets/logo/icon/1024.png"
adaptive_icon_background: "assets/logo/icon/ic_launcher_adaptive_back.png" # only available for Android 8.0 devices and above
adaptive_icon_foreground: "assets/logo/icon/ic_launcher_adaptive_fore.png" # only available for Android 8.0 devices and above
min_sdk_android: 16 # android min sdk min:16, default 21
+1 -1
View File
@@ -54,7 +54,7 @@ flutter_native_splash:
# 640 pixels in diameter.
# App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle
# 768 pixels in diameter.
image: assets/logo/icon-android12.png
image: assets/logo/icon/icon-android12.png
# Splash screen background color.
color: "#993333"
+2
View File
@@ -26,6 +26,8 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Um Fotos direkt aus der App aufnehmen und teilen zu können wird Zugriff auf die Kamera benötigt.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Um Medien mit anderen zu teilen wird Zugriff zu deine Dateien benötigt.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
@@ -1,4 +1,4 @@
class ApiError {
class ApiError implements Exception {
String message;
ApiError(this.message);
+16
View File
@@ -0,0 +1,16 @@
abstract class AppException implements Exception {
final String userMessage;
final String? technicalDetails;
final bool allowRetry;
const AppException({
required this.userMessage,
this.technicalDetails,
this.allowRetry = true,
});
@override
String toString() => technicalDetails == null
? '$runtimeType: $userMessage'
: '$runtimeType: $userMessage ($technicalDetails)';
}
+23
View File
@@ -0,0 +1,23 @@
import 'app_exception.dart';
class AuthException extends AppException {
final int statusCode;
const AuthException({
required this.statusCode,
required super.userMessage,
super.technicalDetails,
}) : super(allowRetry: false);
factory AuthException.unauthorized({String? technicalDetails}) => AuthException(
statusCode: 401,
userMessage: 'Bitte melde dich erneut an, um fortzufahren.',
technicalDetails: technicalDetails,
);
factory AuthException.forbidden({String? technicalDetails}) => AuthException(
statusCode: 403,
userMessage: 'Du hast keine Berechtigung für diese Aktion.',
technicalDetails: technicalDetails,
);
}
+64
View File
@@ -0,0 +1,64 @@
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../api_error.dart';
import '../marianumcloud/talk/talk_error.dart';
import '../webuntis/webuntis_error.dart';
import 'app_exception.dart';
import 'network_exception.dart';
import 'parse_exception.dart';
import 'talk_exception.dart';
import 'webuntis_exception.dart';
const String _defaultFallback = 'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
String errorToUserMessage(Object? error, {String fallback = _defaultFallback}) {
if (error == null) return fallback;
if (error is AppException) return error.userMessage;
if (error is TalkError) return TalkException(error).userMessage;
if (error is WebuntisError) return WebuntisException(error).userMessage;
if (error is SocketException) {
return const NetworkException().userMessage;
}
if (error is TimeoutException) {
return NetworkException.timeout().userMessage;
}
if (error is http.ClientException) {
return const NetworkException().userMessage;
}
if (error is HandshakeException) {
return 'Sichere Verbindung konnte nicht hergestellt werden.';
}
if (error is FormatException) {
return const ParseException().userMessage;
}
if (error is ApiError) {
return _stripDioPrefix(error.message);
}
return fallback;
}
String? errorToTechnicalDetails(Object? error) {
if (error == null) return null;
if (error is AppException) return error.technicalDetails ?? error.toString();
if (error is TalkError) return TalkException(error).technicalDetails;
if (error is WebuntisError) return WebuntisException(error).technicalDetails;
return error.toString();
}
bool errorAllowsRetry(Object? error) {
if (error == null) return true;
if (error is AppException) return error.allowRetry;
return true;
}
String _stripDioPrefix(String raw) {
// ApiError messages embed full request URIs; only surface the first line.
final firstLine = raw.split('\n').first.trim();
return firstLine.isEmpty ? _defaultFallback : firstLine;
}
+13
View File
@@ -0,0 +1,13 @@
import 'app_exception.dart';
class NetworkException extends AppException {
const NetworkException({
super.userMessage = 'Keine Internetverbindung. Bitte prüfe dein Netzwerk und versuche es erneut.',
super.technicalDetails,
}) : super(allowRetry: true);
factory NetworkException.timeout({String? technicalDetails}) => NetworkException(
userMessage: 'Der Server hat zu lange gebraucht. Bitte versuche es erneut.',
technicalDetails: technicalDetails,
);
}
+8
View File
@@ -0,0 +1,8 @@
import 'app_exception.dart';
class NotFoundException extends AppException {
const NotFoundException({
super.userMessage = 'Der angeforderte Eintrag wurde nicht gefunden.',
super.technicalDetails,
}) : super(allowRetry: false);
}
+8
View File
@@ -0,0 +1,8 @@
import 'app_exception.dart';
class ParseException extends AppException {
const ParseException({
super.userMessage = 'Die Antwort des Servers konnte nicht gelesen werden.',
super.technicalDetails,
}) : super(allowRetry: true);
}
+14
View File
@@ -0,0 +1,14 @@
import 'app_exception.dart';
class ServerException extends AppException {
final int statusCode;
ServerException({
required this.statusCode,
String? userMessage,
super.technicalDetails,
}) : super(
userMessage: userMessage ?? 'Der Server hat gerade Probleme (Status $statusCode). Bitte später erneut versuchen.',
allowRetry: true,
);
}
+33
View File
@@ -0,0 +1,33 @@
import '../marianumcloud/talk/talk_error.dart';
import 'app_exception.dart';
class TalkException extends AppException {
final TalkError source;
TalkException(this.source)
: super(
userMessage: _mapMessage(source),
technicalDetails: 'Talk ${source.status} (${source.code}): ${source.message}',
allowRetry: source.code >= 500,
);
static String _mapMessage(TalkError e) {
switch (e.code) {
case 401:
return 'Bitte melde dich erneut an, um auf Talk zuzugreifen.';
case 403:
return 'Du hast keine Berechtigung für diese Talk-Aktion.';
case 404:
return 'Dieser Chat existiert nicht oder wurde entfernt.';
case 412:
return 'Diese Aktion ist im aktuellen Chat-Zustand nicht erlaubt.';
case 429:
return 'Zu viele Anfragen. Bitte kurz warten und erneut versuchen.';
default:
if (e.code >= 500) {
return 'Talk-Server hat gerade Probleme (${e.code}).';
}
return e.message.isNotEmpty ? e.message : 'Talk meldet einen Fehler (${e.code}).';
}
}
}
+31
View File
@@ -0,0 +1,31 @@
import '../webuntis/webuntis_error.dart';
import 'app_exception.dart';
class WebuntisException extends AppException {
final WebuntisError source;
WebuntisException(this.source)
: super(
userMessage: _mapMessage(source),
technicalDetails: 'WebUntis (${source.code}): ${source.message}',
allowRetry: true,
);
static String _mapMessage(WebuntisError e) {
switch (e.code) {
case -8504:
case -8502:
return 'WebUntis-Anmeldung abgelaufen. Bitte erneut anmelden.';
case -8520:
return 'Bitte melde dich erneut an.';
case -7004:
return 'Für diesen Zeitraum sind keine Stundenplandaten verfügbar.';
case -32601:
return 'WebUntis kennt diese Anfrage nicht. Bitte App aktualisieren.';
default:
return e.message.isNotEmpty
? 'WebUntis: ${e.message}'
: 'WebUntis konnte die Anfrage nicht bearbeiten (Code ${e.code}).';
}
}
}
-26
View File
@@ -1,26 +0,0 @@
import 'dart:convert';
import '../requestCache.dart';
import 'getHolidays.dart';
import 'getHolidaysResponse.dart';
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
GetHolidaysCache({onUpdate, renew}) : super(RequestCache.cacheDay, onUpdate, renew: renew) {
start('state-holidays');
}
@override
GetHolidaysResponse onLocalData(String json) {
List<dynamic> parsedListJson = jsonDecode(json)['data'];
return GetHolidaysResponse(
List<GetHolidaysResponseObject>.from(
parsedListJson.map<GetHolidaysResponseObject>(
(i) => GetHolidaysResponseObject.fromJson(i as Map<String, dynamic>)
)
)
);
}
@override
Future<GetHolidaysResponse> onLoad() => GetHolidays().query();
}
@@ -1,46 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'getHolidaysResponse.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetHolidaysResponse _$GetHolidaysResponseFromJson(Map<String, dynamic> json) =>
GetHolidaysResponse(
(json['data'] as List<dynamic>)
.map((e) =>
GetHolidaysResponseObject.fromJson(e as Map<String, dynamic>))
.toList(),
)..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetHolidaysResponseToJson(
GetHolidaysResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetHolidaysResponseObject _$GetHolidaysResponseObjectFromJson(
Map<String, dynamic> json) =>
GetHolidaysResponseObject(
start: json['start'] as String,
end: json['end'] as String,
year: (json['year'] as num).toInt(),
stateCode: json['stateCode'] as String,
name: json['name'] as String,
slug: json['slug'] as String,
);
Map<String, dynamic> _$GetHolidaysResponseObjectToJson(
GetHolidaysResponseObject instance) =>
<String, dynamic>{
'start': instance.start,
'end': instance.end,
'year': instance.year,
'stateCode': instance.stateCode,
'name': instance.name,
'slug': instance.slug,
};
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'getHolidaysResponse.dart';
import 'get_holidays_response.dart';
class GetHolidays {
Future<GetHolidaysResponse> query() async {
+18
View File
@@ -0,0 +1,18 @@
import '../request_cache.dart';
import 'get_holidays.dart';
import 'get_holidays_response.dart';
class GetHolidaysCache extends SimpleCache<GetHolidaysResponse> {
GetHolidaysCache({super.onUpdate, super.renew})
: super(
cacheTime: RequestCache.cacheDay,
loader: () => GetHolidays().query(),
fromJson: (json) => GetHolidaysResponse(
(json['data'] as List)
.map((i) => GetHolidaysResponseObject.fromJson(i as Map<String, dynamic>))
.toList(),
),
) {
start('state-holidays');
}
}
@@ -1,9 +1,9 @@
import 'package:json_annotation/json_annotation.dart';
import '../apiResponse.dart';
import '../api_response.dart';
part 'getHolidaysResponse.g.dart';
part 'get_holidays_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetHolidaysResponse extends ApiResponse {
@@ -0,0 +1,49 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_holidays_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetHolidaysResponse _$GetHolidaysResponseFromJson(Map<String, dynamic> json) =>
GetHolidaysResponse(
(json['data'] as List<dynamic>)
.map(
(e) =>
GetHolidaysResponseObject.fromJson(e as Map<String, dynamic>),
)
.toList(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetHolidaysResponseToJson(
GetHolidaysResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetHolidaysResponseObject _$GetHolidaysResponseObjectFromJson(
Map<String, dynamic> json,
) => GetHolidaysResponseObject(
start: json['start'] as String,
end: json['end'] as String,
year: (json['year'] as num).toInt(),
stateCode: json['stateCode'] as String,
name: json['name'] as String,
slug: json['slug'] as String,
);
Map<String, dynamic> _$GetHolidaysResponseObjectToJson(
GetHolidaysResponseObject instance,
) => <String, dynamic>{
'start': instance.start,
'end': instance.end,
'year': instance.year,
'stateCode': instance.stateCode,
'name': instance.name,
'slug': instance.slug,
};
@@ -1,32 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../../../model/accountData.dart';
import '../../../model/endpointData.dart';
import 'autocompleteResponse.dart';
class AutocompleteApi {
Future<AutocompleteResponse> find(String query) async {
var getParameters = <String, dynamic>{
'search': query,
'itemType': ' ',
'itemId': ' ',
'shareTypes[]': ['0'],
'limit': '10',
};
var headers = <String, String>{};
headers.putIfAbsent('Accept', () => 'application/json');
headers.putIfAbsent('OCS-APIRequest', () => 'true');
var endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/core/autocomplete/get', getParameters);
var response = await http.get(endpoint, headers: headers);
if(response.statusCode != HttpStatus.ok) throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
var result = response.body;
return AutocompleteResponse.fromJson(jsonDecode(result)['ocs']);
}
}
@@ -1,46 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'autocompleteResponse.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AutocompleteResponse _$AutocompleteResponseFromJson(
Map<String, dynamic> json) =>
AutocompleteResponse(
(json['data'] as List<dynamic>)
.map((e) =>
AutocompleteResponseObject.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$AutocompleteResponseToJson(
AutocompleteResponse instance) =>
<String, dynamic>{
'data': instance.data.map((e) => e.toJson()).toList(),
};
AutocompleteResponseObject _$AutocompleteResponseObjectFromJson(
Map<String, dynamic> json) =>
AutocompleteResponseObject(
json['id'] as String,
json['label'] as String,
json['icon'] as String?,
json['source'] as String?,
json['status'] as String?,
json['subline'] as String?,
json['shareWithDisplayNameUniqe'] as String?,
);
Map<String, dynamic> _$AutocompleteResponseObjectToJson(
AutocompleteResponseObject instance) =>
<String, dynamic>{
'id': instance.id,
'label': instance.label,
'icon': instance.icon,
'source': instance.source,
'status': instance.status,
'subline': instance.subline,
'shareWithDisplayNameUniqe': instance.shareWithDisplayNameUniqe,
};
@@ -0,0 +1,28 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../nextcloud_ocs.dart';
import 'autocomplete_response.dart';
class AutocompleteApi {
Future<AutocompleteResponse> find(String query) async {
final endpoint = NextcloudOcs.uri(
'core/autocomplete/get',
queryParameters: {
'search': query,
'itemType': ' ',
'itemId': ' ',
'shareTypes[]': ['0'],
'limit': '10',
},
);
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
if (response.statusCode != HttpStatus.ok) {
throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
}
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
return AutocompleteResponse.fromJson(decoded['ocs'] as Map<String, dynamic>);
}
}
@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
part 'autocompleteResponse.g.dart';
part 'autocomplete_response.g.dart';
@JsonSerializable(explicitToJson: true)
class AutocompleteResponse {
@@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'autocomplete_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AutocompleteResponse _$AutocompleteResponseFromJson(
Map<String, dynamic> json,
) => AutocompleteResponse(
(json['data'] as List<dynamic>)
.map(
(e) => AutocompleteResponseObject.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$AutocompleteResponseToJson(
AutocompleteResponse instance,
) => <String, dynamic>{'data': instance.data.map((e) => e.toJson()).toList()};
AutocompleteResponseObject _$AutocompleteResponseObjectFromJson(
Map<String, dynamic> json,
) => AutocompleteResponseObject(
json['id'] as String,
json['label'] as String,
json['icon'] as String?,
json['source'] as String?,
json['status'] as String?,
json['subline'] as String?,
json['shareWithDisplayNameUniqe'] as String?,
);
Map<String, dynamic> _$AutocompleteResponseObjectToJson(
AutocompleteResponseObject instance,
) => <String, dynamic>{
'id': instance.id,
'label': instance.label,
'icon': instance.icon,
'source': instance.source,
'status': instance.status,
'subline': instance.subline,
'shareWithDisplayNameUniqe': instance.shareWithDisplayNameUniqe,
};
@@ -1,22 +0,0 @@
import 'dart:io';
import 'package:http/http.dart' as http;
import '../../../model/accountData.dart';
import '../../../model/endpointData.dart';
import 'fileSharingApiParams.dart';
class FileSharingApi {
Future<void> share(FileSharingApiParams query) async {
var headers = <String, String>{};
headers.putIfAbsent('Accept', () => 'application/json');
headers.putIfAbsent('OCS-APIRequest', () => 'true');
var endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/apps/files_sharing/api/v1/shares', query.toJson().map((key, value) => MapEntry(key, value.toString())));
var response = await http.post(endpoint, headers: headers);
if(response.statusCode != HttpStatus.ok) {
throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
}
}
}
@@ -1,27 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'fileSharingApiParams.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FileSharingApiParams _$FileSharingApiParamsFromJson(
Map<String, dynamic> json) =>
FileSharingApiParams(
shareType: (json['shareType'] as num).toInt(),
shareWith: json['shareWith'] as String,
path: json['path'] as String,
referenceId: json['referenceId'] as String?,
talkMetaData: json['talkMetaData'] as String?,
);
Map<String, dynamic> _$FileSharingApiParamsToJson(
FileSharingApiParams instance) =>
<String, dynamic>{
'shareType': instance.shareType,
'shareWith': instance.shareWith,
'path': instance.path,
'referenceId': instance.referenceId,
'talkMetaData': instance.talkMetaData,
};
@@ -0,0 +1,19 @@
import 'dart:io';
import 'package:http/http.dart' as http;
import '../nextcloud_ocs.dart';
import 'file_sharing_api_params.dart';
class FileSharingApi {
Future<void> share(FileSharingApiParams query) async {
final endpoint = NextcloudOcs.uri(
'apps/files_sharing/api/v1/shares',
queryParameters: query.toJson(),
);
final response = await http.post(endpoint, headers: NextcloudOcs.headers());
if (response.statusCode != HttpStatus.ok) {
throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
}
}
}
@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
part 'fileSharingApiParams.g.dart';
part 'file_sharing_api_params.g.dart';
@JsonSerializable()
class FileSharingApiParams {
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'file_sharing_api_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FileSharingApiParams _$FileSharingApiParamsFromJson(
Map<String, dynamic> json,
) => FileSharingApiParams(
shareType: (json['shareType'] as num).toInt(),
shareWith: json['shareWith'] as String,
path: json['path'] as String,
referenceId: json['referenceId'] as String?,
talkMetaData: json['talkMetaData'] as String?,
);
Map<String, dynamic> _$FileSharingApiParamsToJson(
FileSharingApiParams instance,
) => <String, dynamic>{
'shareType': instance.shareType,
'shareWith': instance.shareWith,
'path': instance.path,
'referenceId': instance.referenceId,
'talkMetaData': instance.talkMetaData,
};
+33
View File
@@ -0,0 +1,33 @@
import '../../model/account_data.dart';
import '../../model/endpoint_data.dart';
/// Shared helpers for Nextcloud OCS v2 endpoints.
///
/// Three call sites previously duplicated the same header dictionary and the
/// same URI scaffolding (TalkApi, AutocompleteApi, FileSharingApi). Anything
/// that talks to `https://<domain>/<base>/ocs/v2.php/...` should go through
/// these two helpers so additions like a new header or a different auth
/// scheme only need to change here.
class NextcloudOcs {
NextcloudOcs._();
/// The standard OCS request header set: JSON accept, OCS API marker,
/// HTTP Basic auth from the active [AccountData].
static Map<String, String> headers() => {
'Accept': 'application/json',
'OCS-APIRequest': 'true',
'Authorization': AccountData().getBasicAuthHeader(),
};
/// Builds an OCS URI by appending [pathSuffix] under `/ocs/v2.php/` of
/// the configured Nextcloud endpoint. Query parameters are converted to
/// strings (Uri rejects non-string values).
static Uri uri(String pathSuffix, {Map<String, dynamic>? queryParameters}) {
final endpoint = EndpointData().nextcloud();
return Uri.https(
endpoint.domain,
'${endpoint.path}/ocs/v2.php/$pathSuffix',
queryParameters?.map((key, value) => MapEntry(key, value.toString())),
);
}
}
@@ -0,0 +1,50 @@
import 'package:http/http.dart' as http;
import '../../../api_params.dart';
import '../../../api_response.dart';
import '../talk_api.dart';
/// Small POST/DELETE-only Talk endpoints that have no response payload.
/// Each class extends [TalkApi] with `assemble` returning `null`. They share
/// no state — they're collected here purely to avoid eight near-empty files.
class SetFavorite extends TalkApi {
final String chatToken;
final bool favoriteState;
SetFavorite(this.chatToken, this.favoriteState) : super('v4/room/$chatToken/favorite', null);
@override
ApiResponse? assemble(String raw) => null;
@override
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) =>
favoriteState ? http.post(uri, headers: headers) : http.delete(uri, headers: headers);
}
class LeaveRoom extends TalkApi {
final String chatToken;
LeaveRoom(this.chatToken) : super('v4/room/$chatToken/participants/self', null);
@override
ApiResponse? assemble(String raw) => null;
@override
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) =>
http.delete(uri, headers: headers);
}
class DeleteMessage extends TalkApi {
final String chatToken;
final int messageId;
DeleteMessage(this.chatToken, this.messageId) : super('v1/chat/$chatToken/$messageId', null);
@override
ApiResponse? assemble(String raw) => null;
@override
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) =>
http.delete(uri, headers: headers);
}
@@ -1,28 +0,0 @@
import 'dart:convert';
import '../../../requestCache.dart';
import 'getChat.dart';
import 'getChatParams.dart';
import 'getChatResponse.dart';
class GetChatCache extends RequestCache<GetChatResponse> {
String chatToken;
GetChatCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
start('nc-chat-$chatToken');
}
@override
Future<GetChatResponse> onLoad() => GetChat(
chatToken,
GetChatParams(
lookIntoFuture: GetChatParamsSwitch.off,
setReadMarker: GetChatParamsSwitch.on,
limit: 200,
)
).run();
@override
GetChatResponse onLocalData(String json) => GetChatResponse.fromJson(jsonDecode(json));
}
@@ -1,43 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'getChatParams.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetChatParams _$GetChatParamsFromJson(Map<String, dynamic> json) =>
GetChatParams(
lookIntoFuture:
$enumDecode(_$GetChatParamsSwitchEnumMap, json['lookIntoFuture']),
limit: (json['limit'] as num?)?.toInt(),
lastKnownMessageId: (json['lastKnownMessageId'] as num?)?.toInt(),
lastCommonReadId: (json['lastCommonReadId'] as num?)?.toInt(),
timeout: (json['timeout'] as num?)?.toInt(),
setReadMarker: $enumDecodeNullable(
_$GetChatParamsSwitchEnumMap, json['setReadMarker']),
includeLastKnown: $enumDecodeNullable(
_$GetChatParamsSwitchEnumMap, json['includeLastKnown']),
);
Map<String, dynamic> _$GetChatParamsToJson(GetChatParams instance) =>
<String, dynamic>{
'lookIntoFuture': _$GetChatParamsSwitchEnumMap[instance.lookIntoFuture]!,
if (instance.limit case final value?) 'limit': value,
if (instance.lastKnownMessageId case final value?)
'lastKnownMessageId': value,
if (instance.lastCommonReadId case final value?)
'lastCommonReadId': value,
if (instance.timeout case final value?) 'timeout': value,
if (_$GetChatParamsSwitchEnumMap[instance.setReadMarker]
case final value?)
'setReadMarker': value,
if (_$GetChatParamsSwitchEnumMap[instance.includeLastKnown]
case final value?)
'includeLastKnown': value,
};
const _$GetChatParamsSwitchEnumMap = {
GetChatParamsSwitch.on: 1,
GetChatParamsSwitch.off: 0,
};
@@ -3,9 +3,9 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import '../talkApi.dart';
import 'getChatParams.dart';
import 'getChatResponse.dart';
import '../talk_api.dart';
import 'get_chat_params.dart';
import 'get_chat_response.dart';
class GetChat extends TalkApi<GetChatResponse> {
String chatToken;
@@ -14,7 +14,10 @@ class GetChat extends TalkApi<GetChatResponse> {
GetChat(this.chatToken, this.params) : super('v1/chat/$chatToken', null, getParameters: params.toJson());
@override
assemble(String raw) => GetChatResponse.fromJson(jsonDecode(raw)['ocs']);
GetChatResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetChatResponse.fromJson(decoded['ocs'] as Map<String, dynamic>);
}
@override
Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers);
@@ -0,0 +1,26 @@
import '../../../request_cache.dart';
import 'get_chat.dart';
import 'get_chat_params.dart';
import 'get_chat_response.dart';
class GetChatCache extends SimpleCache<GetChatResponse> {
GetChatCache({
super.onCacheData,
super.onNetworkData,
super.onError,
required String chatToken,
}) : super(
cacheTime: RequestCache.cacheNothing,
loader: () => GetChat(
chatToken,
GetChatParams(
lookIntoFuture: GetChatParamsSwitch.off,
setReadMarker: GetChatParamsSwitch.on,
limit: 200,
),
).run(),
fromJson: GetChatResponse.fromJson,
) {
start('nc-chat-$chatToken');
}
}
@@ -1,8 +1,8 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../apiParams.dart';
import '../../../api_params.dart';
part 'getChatParams.g.dart';
part 'get_chat_params.g.dart';
@JsonSerializable(explicitToJson: true, includeIfNull: false)
class GetChatParams extends ApiParams {
@@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_chat_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetChatParams _$GetChatParamsFromJson(Map<String, dynamic> json) =>
GetChatParams(
lookIntoFuture: $enumDecode(
_$GetChatParamsSwitchEnumMap,
json['lookIntoFuture'],
),
limit: (json['limit'] as num?)?.toInt(),
lastKnownMessageId: (json['lastKnownMessageId'] as num?)?.toInt(),
lastCommonReadId: (json['lastCommonReadId'] as num?)?.toInt(),
timeout: (json['timeout'] as num?)?.toInt(),
setReadMarker: $enumDecodeNullable(
_$GetChatParamsSwitchEnumMap,
json['setReadMarker'],
),
includeLastKnown: $enumDecodeNullable(
_$GetChatParamsSwitchEnumMap,
json['includeLastKnown'],
),
);
Map<String, dynamic> _$GetChatParamsToJson(
GetChatParams instance,
) => <String, dynamic>{
'lookIntoFuture': _$GetChatParamsSwitchEnumMap[instance.lookIntoFuture]!,
'limit': ?instance.limit,
'lastKnownMessageId': ?instance.lastKnownMessageId,
'lastCommonReadId': ?instance.lastCommonReadId,
'timeout': ?instance.timeout,
'setReadMarker': ?_$GetChatParamsSwitchEnumMap[instance.setReadMarker],
'includeLastKnown': ?_$GetChatParamsSwitchEnumMap[instance.includeLastKnown],
};
const _$GetChatParamsSwitchEnumMap = {
GetChatParamsSwitch.on: 1,
GetChatParamsSwitch.off: 0,
};
@@ -1,10 +1,10 @@
import 'package:jiffy/jiffy.dart';
import 'package:json_annotation/json_annotation.dart';
import '../../../apiResponse.dart';
import '../room/getRoomResponse.dart';
import '../../../api_response.dart';
import '../room/get_room_response.dart';
part 'getChatResponse.g.dart';
part 'get_chat_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetChatResponse extends ApiResponse {
@@ -86,11 +86,11 @@ class GetChatResponseObject {
}
Map<String, RichObjectString>? _fromJson(json) {
if(json is Map<String, dynamic>) {
var data = <String, RichObjectString>{};
for (var element in json.keys) {
data.putIfAbsent(element, () => RichObjectString.fromJson(json[element]));
Map<String, RichObjectString>? _fromJson(dynamic json) {
if (json is Map<String, dynamic>) {
final data = <String, RichObjectString>{};
for (final element in json.keys) {
data.putIfAbsent(element, () => RichObjectString.fromJson(json[element] as Map<String, dynamic>));
}
return data;
}
@@ -121,4 +121,5 @@ enum RichObjectStringObjectType {
@JsonValue('highlight') highlight,
@JsonValue('talk-poll') talkPoll,
@JsonValue('geo-location') geoLocation,
@JsonValue('call') call,
}
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'getChatResponse.dart';
part of 'get_chat_response.dart';
// **************************************************************************
// JsonSerializableGenerator
@@ -8,70 +8,72 @@ part of 'getChatResponse.dart';
GetChatResponse _$GetChatResponseFromJson(Map<String, dynamic> json) =>
GetChatResponse(
(json['data'] as List<dynamic>)
.map((e) => GetChatResponseObject.fromJson(e as Map<String, dynamic>))
.toSet(),
)..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(json['data'] as List<dynamic>)
.map(
(e) => GetChatResponseObject.fromJson(e as Map<String, dynamic>),
)
.toSet(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetChatResponseToJson(GetChatResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'headers': ?instance.headers,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetChatResponseObject _$GetChatResponseObjectFromJson(
Map<String, dynamic> json) =>
GetChatResponseObject(
(json['id'] as num).toInt(),
json['token'] as String,
$enumDecode(
_$GetRoomResponseObjectMessageActorTypeEnumMap, json['actorType']),
json['actorId'] as String,
json['actorDisplayName'] as String,
(json['timestamp'] as num).toInt(),
json['systemMessage'] as String,
$enumDecode(
_$GetRoomResponseObjectMessageTypeEnumMap, json['messageType']),
json['isReplyable'] as bool,
json['referenceId'] as String,
json['message'] as String,
_fromJson(json['messageParameters']),
(json['reactions'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()),
),
(json['reactionsSelf'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
json['parent'] == null
? null
: GetChatResponseObject.fromJson(
json['parent'] as Map<String, dynamic>),
);
Map<String, dynamic> json,
) => GetChatResponseObject(
(json['id'] as num).toInt(),
json['token'] as String,
$enumDecode(
_$GetRoomResponseObjectMessageActorTypeEnumMap,
json['actorType'],
),
json['actorId'] as String,
json['actorDisplayName'] as String,
(json['timestamp'] as num).toInt(),
json['systemMessage'] as String,
$enumDecode(_$GetRoomResponseObjectMessageTypeEnumMap, json['messageType']),
json['isReplyable'] as bool,
json['referenceId'] as String,
json['message'] as String,
_fromJson(json['messageParameters']),
(json['reactions'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()),
),
(json['reactionsSelf'] as List<dynamic>?)?.map((e) => e as String).toList(),
json['parent'] == null
? null
: GetChatResponseObject.fromJson(json['parent'] as Map<String, dynamic>),
);
Map<String, dynamic> _$GetChatResponseObjectToJson(
GetChatResponseObject instance) =>
<String, dynamic>{
'id': instance.id,
'token': instance.token,
'actorType':
_$GetRoomResponseObjectMessageActorTypeEnumMap[instance.actorType]!,
'actorId': instance.actorId,
'actorDisplayName': instance.actorDisplayName,
'timestamp': instance.timestamp,
'systemMessage': instance.systemMessage,
'messageType':
_$GetRoomResponseObjectMessageTypeEnumMap[instance.messageType]!,
'isReplyable': instance.isReplyable,
'referenceId': instance.referenceId,
'message': instance.message,
'reactions': instance.reactions,
'reactionsSelf': instance.reactionsSelf,
'messageParameters':
instance.messageParameters?.map((k, e) => MapEntry(k, e.toJson())),
'parent': instance.parent?.toJson(),
};
GetChatResponseObject instance,
) => <String, dynamic>{
'id': instance.id,
'token': instance.token,
'actorType':
_$GetRoomResponseObjectMessageActorTypeEnumMap[instance.actorType]!,
'actorId': instance.actorId,
'actorDisplayName': instance.actorDisplayName,
'timestamp': instance.timestamp,
'systemMessage': instance.systemMessage,
'messageType':
_$GetRoomResponseObjectMessageTypeEnumMap[instance.messageType]!,
'isReplyable': instance.isReplyable,
'referenceId': instance.referenceId,
'message': instance.message,
'reactions': instance.reactions,
'reactionsSelf': instance.reactionsSelf,
'messageParameters': instance.messageParameters?.map(
(k, e) => MapEntry(k, e.toJson()),
),
'parent': instance.parent?.toJson(),
};
const _$GetRoomResponseObjectMessageActorTypeEnumMap = {
GetRoomResponseObjectMessageActorType.deletedUsers: 'deleted_users',
@@ -83,6 +85,7 @@ const _$GetRoomResponseObjectMessageActorTypeEnumMap = {
const _$GetRoomResponseObjectMessageTypeEnumMap = {
GetRoomResponseObjectMessageType.comment: 'comment',
GetRoomResponseObjectMessageType.voiceMessage: 'voice-message',
GetRoomResponseObjectMessageType.deletedComment: 'comment_deleted',
GetRoomResponseObjectMessageType.system: 'system',
GetRoomResponseObjectMessageType.command: 'command',
@@ -114,4 +117,5 @@ const _$RichObjectStringObjectTypeEnumMap = {
RichObjectStringObjectType.highlight: 'highlight',
RichObjectStringObjectType.talkPoll: 'talk-poll',
RichObjectStringObjectType.geoLocation: 'geo-location',
RichObjectStringObjectType.call: 'call',
};
@@ -1,5 +1,5 @@
import 'getChatResponse.dart';
import 'get_chat_response.dart';
class RichObjectStringProcessor {
static String parseToString(String message, Map<String, RichObjectString>? data) {
@@ -2,15 +2,15 @@
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import '../talkApi.dart';
import 'createRoomParams.dart';
import '../talk_api.dart';
import 'create_room_params.dart';
class CreateRoom extends TalkApi {
CreateRoomParams params;
CreateRoom(this.params) : super('v4/room', params);
@override
assemble(String raw) => null;
Null assemble(String raw) => null;
@override
Future<Response>? request(Uri uri, Object? body, Map<String, String>? headers) {
@@ -1,8 +1,8 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../apiParams.dart';
import '../../../api_params.dart';
part 'createRoomParams.g.dart';
part 'create_room_params.g.dart';
@JsonSerializable()
class CreateRoomParams extends ApiParams {
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'createRoomParams.dart';
part of 'create_room_params.dart';
// **************************************************************************
// JsonSerializableGenerator
@@ -1,18 +0,0 @@
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import '../../../apiParams.dart';
import '../talkApi.dart';
class DeleteMessage extends TalkApi {
String chatToken;
int messageId;
DeleteMessage(this.chatToken, this.messageId) : super('v1/chat/$chatToken/$messageId', null);
@override
assemble(String raw) => null;
@override
Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) => http.delete(uri, headers: headers);
}
@@ -1,9 +1,9 @@
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import '../../../apiParams.dart';
import '../talkApi.dart';
import 'deleteReactMessageParams.dart';
import '../../../api_params.dart';
import '../talk_api.dart';
import 'delete_react_message_params.dart';
class DeleteReactMessage extends TalkApi {
String chatToken;
@@ -11,7 +11,7 @@ class DeleteReactMessage extends TalkApi {
DeleteReactMessage({required this.chatToken, required this.messageId, required DeleteReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params);
@override
assemble(String raw) => null;
Null assemble(String raw) => null;
@override
Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) {
@@ -1,8 +1,8 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../apiParams.dart';
import '../../../api_params.dart';
part 'deleteReactMessageParams.g.dart';
part 'delete_react_message_params.g.dart';
@JsonSerializable()
class DeleteReactMessageParams extends ApiParams {
@@ -1,19 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'deleteReactMessageParams.dart';
part of 'delete_react_message_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DeleteReactMessageParams _$DeleteReactMessageParamsFromJson(
Map<String, dynamic> json) =>
DeleteReactMessageParams(
json['reaction'] as String,
);
Map<String, dynamic> json,
) => DeleteReactMessageParams(json['reaction'] as String);
Map<String, dynamic> _$DeleteReactMessageParamsToJson(
DeleteReactMessageParams instance) =>
<String, dynamic>{
'reaction': instance.reaction,
};
DeleteReactMessageParams instance,
) => <String, dynamic>{'reaction': instance.reaction};
@@ -1,22 +0,0 @@
import 'dart:convert';
import '../../../requestCache.dart';
import 'getParticipants.dart';
import 'getParticipantsResponse.dart';
class GetParticipantsCache extends RequestCache<GetParticipantsResponse> {
String chatToken;
GetParticipantsCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
start('nc-chat-participants-$chatToken');
}
@override
Future<GetParticipantsResponse> onLoad() => GetParticipants(
chatToken,
).run();
@override
GetParticipantsResponse onLocalData(String json) => GetParticipantsResponse.fromJson(jsonDecode(json));
}
@@ -1,86 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'getParticipantsResponse.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetParticipantsResponse _$GetParticipantsResponseFromJson(
Map<String, dynamic> json) =>
GetParticipantsResponse(
(json['data'] as List<dynamic>)
.map((e) =>
GetParticipantsResponseObject.fromJson(e as Map<String, dynamic>))
.toSet(),
)..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetParticipantsResponseToJson(
GetParticipantsResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetParticipantsResponseObject _$GetParticipantsResponseObjectFromJson(
Map<String, dynamic> json) =>
GetParticipantsResponseObject(
(json['attendeeId'] as num).toInt(),
json['actorType'] as String,
json['actorId'] as String,
json['displayName'] as String,
$enumDecode(_$GetParticipantsResponseObjectParticipantTypeEnumMap,
json['participantType']),
(json['lastPing'] as num).toInt(),
$enumDecode(_$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap,
json['inCall']),
(json['permissions'] as num).toInt(),
(json['attendeePermissions'] as num).toInt(),
json['sessionId'] as String?,
(json['sessionIds'] as List<dynamic>).map((e) => e as String).toList(),
json['status'] as String?,
json['statusIcon'] as String?,
json['statusMessage'] as String?,
json['roomToken'] as String?,
);
Map<String, dynamic> _$GetParticipantsResponseObjectToJson(
GetParticipantsResponseObject instance) =>
<String, dynamic>{
'attendeeId': instance.attendeeId,
'actorType': instance.actorType,
'actorId': instance.actorId,
'displayName': instance.displayName,
'participantType': _$GetParticipantsResponseObjectParticipantTypeEnumMap[
instance.participantType]!,
'lastPing': instance.lastPing,
'inCall': _$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap[
instance.inCall]!,
'permissions': instance.permissions,
'attendeePermissions': instance.attendeePermissions,
'sessionId': instance.sessionId,
'sessionIds': instance.sessionIds,
'status': instance.status,
'statusIcon': instance.statusIcon,
'statusMessage': instance.statusMessage,
'roomToken': instance.roomToken,
};
const _$GetParticipantsResponseObjectParticipantTypeEnumMap = {
GetParticipantsResponseObjectParticipantType.owner: 1,
GetParticipantsResponseObjectParticipantType.moderator: 2,
GetParticipantsResponseObjectParticipantType.user: 3,
GetParticipantsResponseObjectParticipantType.guest: 4,
GetParticipantsResponseObjectParticipantType.userFollowingPublicLink: 5,
GetParticipantsResponseObjectParticipantType.guestWithModeratorPermissions: 6,
};
const _$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap = {
GetParticipantsResponseObjectParticipantsInCallFlags.disconnected: 0,
GetParticipantsResponseObjectParticipantsInCallFlags.inCall: 1,
GetParticipantsResponseObjectParticipantsInCallFlags.providesAudio: 2,
GetParticipantsResponseObjectParticipantsInCallFlags.providesVideo: 3,
GetParticipantsResponseObjectParticipantsInCallFlags.usesSipDialIn: 4,
};
@@ -1,55 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'getReactionsResponse.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetReactionsResponse _$GetReactionsResponseFromJson(
Map<String, dynamic> json) =>
GetReactionsResponse(
(json['data'] as Map<String, dynamic>).map(
(k, e) => MapEntry(
k,
(e as List<dynamic>)
.map((e) => GetReactionsResponseObject.fromJson(
e as Map<String, dynamic>))
.toList()),
),
)..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetReactionsResponseToJson(
GetReactionsResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'data': instance.data
.map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList())),
};
GetReactionsResponseObject _$GetReactionsResponseObjectFromJson(
Map<String, dynamic> json) =>
GetReactionsResponseObject(
$enumDecode(
_$GetReactionsResponseObjectActorTypeEnumMap, json['actorType']),
json['actorId'] as String,
json['actorDisplayName'] as String,
(json['timestamp'] as num).toInt(),
);
Map<String, dynamic> _$GetReactionsResponseObjectToJson(
GetReactionsResponseObject instance) =>
<String, dynamic>{
'actorType':
_$GetReactionsResponseObjectActorTypeEnumMap[instance.actorType]!,
'actorId': instance.actorId,
'actorDisplayName': instance.actorDisplayName,
'timestamp': instance.timestamp,
};
const _$GetReactionsResponseObjectActorTypeEnumMap = {
GetReactionsResponseObjectActorType.guests: 'guests',
GetReactionsResponseObjectActorType.users: 'users',
};
@@ -2,15 +2,18 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import '../talkApi.dart';
import 'getParticipantsResponse.dart';
import '../talk_api.dart';
import 'get_participants_response.dart';
class GetParticipants extends TalkApi<GetParticipantsResponse> {
String token;
GetParticipants(this.token) : super('v4/room/$token/participants', null);
@override
GetParticipantsResponse assemble(String raw) => GetParticipantsResponse.fromJson(jsonDecode(raw)['ocs']);
GetParticipantsResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetParticipantsResponse.fromJson(decoded['ocs'] as Map<String, dynamic>);
}
@override
Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers);
@@ -0,0 +1,17 @@
import '../../../request_cache.dart';
import 'get_participants.dart';
import 'get_participants_response.dart';
class GetParticipantsCache extends SimpleCache<GetParticipantsResponse> {
GetParticipantsCache({
required void Function(GetParticipantsResponse) onUpdate,
required String chatToken,
}) : super(
cacheTime: RequestCache.cacheNothing,
loader: () => GetParticipants(chatToken).run(),
fromJson: GetParticipantsResponse.fromJson,
onUpdate: onUpdate,
) {
start('nc-chat-participants-$chatToken');
}
}
@@ -1,9 +1,9 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../apiResponse.dart';
import '../../../api_response.dart';
part 'getParticipantsResponse.g.dart';
part 'get_participants_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetParticipantsResponse extends ApiResponse {
@@ -0,0 +1,97 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_participants_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetParticipantsResponse _$GetParticipantsResponseFromJson(
Map<String, dynamic> json,
) =>
GetParticipantsResponse(
(json['data'] as List<dynamic>)
.map(
(e) => GetParticipantsResponseObject.fromJson(
e as Map<String, dynamic>,
),
)
.toSet(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetParticipantsResponseToJson(
GetParticipantsResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetParticipantsResponseObject _$GetParticipantsResponseObjectFromJson(
Map<String, dynamic> json,
) => GetParticipantsResponseObject(
(json['attendeeId'] as num).toInt(),
json['actorType'] as String,
json['actorId'] as String,
json['displayName'] as String,
$enumDecode(
_$GetParticipantsResponseObjectParticipantTypeEnumMap,
json['participantType'],
),
(json['lastPing'] as num).toInt(),
$enumDecode(
_$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap,
json['inCall'],
),
(json['permissions'] as num).toInt(),
(json['attendeePermissions'] as num).toInt(),
json['sessionId'] as String?,
(json['sessionIds'] as List<dynamic>).map((e) => e as String).toList(),
json['status'] as String?,
json['statusIcon'] as String?,
json['statusMessage'] as String?,
json['roomToken'] as String?,
);
Map<String, dynamic> _$GetParticipantsResponseObjectToJson(
GetParticipantsResponseObject instance,
) => <String, dynamic>{
'attendeeId': instance.attendeeId,
'actorType': instance.actorType,
'actorId': instance.actorId,
'displayName': instance.displayName,
'participantType':
_$GetParticipantsResponseObjectParticipantTypeEnumMap[instance
.participantType]!,
'lastPing': instance.lastPing,
'inCall':
_$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap[instance
.inCall]!,
'permissions': instance.permissions,
'attendeePermissions': instance.attendeePermissions,
'sessionId': instance.sessionId,
'sessionIds': instance.sessionIds,
'status': instance.status,
'statusIcon': instance.statusIcon,
'statusMessage': instance.statusMessage,
'roomToken': instance.roomToken,
};
const _$GetParticipantsResponseObjectParticipantTypeEnumMap = {
GetParticipantsResponseObjectParticipantType.owner: 1,
GetParticipantsResponseObjectParticipantType.moderator: 2,
GetParticipantsResponseObjectParticipantType.user: 3,
GetParticipantsResponseObjectParticipantType.guest: 4,
GetParticipantsResponseObjectParticipantType.userFollowingPublicLink: 5,
GetParticipantsResponseObjectParticipantType.guestWithModeratorPermissions: 6,
};
const _$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap = {
GetParticipantsResponseObjectParticipantsInCallFlags.disconnected: 0,
GetParticipantsResponseObjectParticipantsInCallFlags.inCall: 1,
GetParticipantsResponseObjectParticipantsInCallFlags.providesAudio: 2,
GetParticipantsResponseObjectParticipantsInCallFlags.providesVideo: 3,
GetParticipantsResponseObjectParticipantsInCallFlags.usesSipDialIn: 4,
};
@@ -0,0 +1,21 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../talk_api.dart';
import 'get_poll_state_response.dart';
class GetPollState extends TalkApi<GetPollStateResponse> {
String token;
int pollId;
GetPollState({required this.token, required this.pollId}) : super('v1/poll/$token/$pollId', null);
@override
GetPollStateResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetPollStateResponse.fromJson(decoded['ocs'] as Map<String, dynamic>);
}
@override
Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers);
}
@@ -0,0 +1,50 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'get_poll_state_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetPollStateResponse extends ApiResponse {
GetPollStateResponseObject data;
GetPollStateResponse(this.data);
factory GetPollStateResponse.fromJson(Map<String, dynamic> json) => _$GetPollStateResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetPollStateResponseToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetPollStateResponseObject {
int id;
String question;
List<String> options;
dynamic votes;
String actorType;
String actorId;
String actorDisplayName;
int status;
int resultMode;
int maxVotes;
List<int> votedSelf;
int? numVoters;
List<dynamic>? details;
GetPollStateResponseObject(
this.id,
this.question,
this.options,
this.votes,
this.actorType,
this.actorId,
this.actorDisplayName,
this.status,
this.resultMode,
this.maxVotes,
this.votedSelf,
this.numVoters,
this.details);
factory GetPollStateResponseObject.fromJson(Map<String, dynamic> json) => _$GetPollStateResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetPollStateResponseObjectToJson(this);
}
@@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_poll_state_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetPollStateResponse _$GetPollStateResponseFromJson(
Map<String, dynamic> json,
) =>
GetPollStateResponse(
GetPollStateResponseObject.fromJson(
json['data'] as Map<String, dynamic>,
),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetPollStateResponseToJson(
GetPollStateResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'data': instance.data.toJson(),
};
GetPollStateResponseObject _$GetPollStateResponseObjectFromJson(
Map<String, dynamic> json,
) => GetPollStateResponseObject(
(json['id'] as num).toInt(),
json['question'] as String,
(json['options'] as List<dynamic>).map((e) => e as String).toList(),
json['votes'],
json['actorType'] as String,
json['actorId'] as String,
json['actorDisplayName'] as String,
(json['status'] as num).toInt(),
(json['resultMode'] as num).toInt(),
(json['maxVotes'] as num).toInt(),
(json['votedSelf'] as List<dynamic>).map((e) => (e as num).toInt()).toList(),
(json['numVoters'] as num?)?.toInt(),
json['details'] as List<dynamic>?,
);
Map<String, dynamic> _$GetPollStateResponseObjectToJson(
GetPollStateResponseObject instance,
) => <String, dynamic>{
'id': instance.id,
'question': instance.question,
'options': instance.options,
'votes': instance.votes,
'actorType': instance.actorType,
'actorId': instance.actorId,
'actorDisplayName': instance.actorDisplayName,
'status': instance.status,
'resultMode': instance.resultMode,
'maxVotes': instance.maxVotes,
'votedSelf': instance.votedSelf,
'numVoters': instance.numVoters,
'details': instance.details,
};
@@ -3,9 +3,9 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import '../../../apiParams.dart';
import '../talkApi.dart';
import 'getReactionsResponse.dart';
import '../../../api_params.dart';
import '../talk_api.dart';
import 'get_reactions_response.dart';
class GetReactions extends TalkApi<GetReactionsResponse> {
String chatToken;
@@ -13,7 +13,10 @@ class GetReactions extends TalkApi<GetReactionsResponse> {
GetReactions({required this.chatToken, required this.messageId}) : super('v1/reaction/$chatToken/$messageId', null);
@override
assemble(String raw) => GetReactionsResponse.fromJson(jsonDecode(raw)['ocs']);
GetReactionsResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetReactionsResponse.fromJson(decoded['ocs'] as Map<String, dynamic>);
}
@override
Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) => http.get(uri, headers: headers);
@@ -1,8 +1,8 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../apiResponse.dart';
import '../../../api_response.dart';
part 'getReactionsResponse.g.dart';
part 'get_reactions_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetReactionsResponse extends ApiResponse {

Some files were not shown because too many files have changed in this diff Show More