Compare commits
No commits in common. "master" and "notevault-3" have entirely different histories.
master
...
notevault-
46
.gitea/workflows/job.yaml
Normal file
46
.gitea/workflows/job.yaml
Normal file
@ -0,0 +1,46 @@
|
||||
+name: Build and Upload APK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- notevault-3 # Trigger für den Hauptbranch
|
||||
pull_request:
|
||||
branches:
|
||||
- notevault-3 # Trigger für Pull Requests in den Hauptbranch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Schritt 1: Checkout des Repositories
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Schritt 2: Setup JDK
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
# Schritt 3: Installiere Gradle und Baue die APK
|
||||
- name: Build APK
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wget unzip
|
||||
wget https://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
unzip gradle-7.4-bin.zip
|
||||
export PATH=$PWD/gradle-7.4/bin:$PATH
|
||||
./gradlew clean assembleRelease
|
||||
|
||||
# Schritt 4: Upload der APK
|
||||
- name: Upload APK to Gitea Releases
|
||||
uses: pappasam/gitea-release-action@v1
|
||||
with:
|
||||
gitea_token: ${{ secrets.REGISTRY_PASSWORD }} # Dein Gitea API-Token
|
||||
gitea_url: 'https://git.fawkes100.de' # Deine Gitea-URL
|
||||
owner: ${{ secrets.REGISTRY_USER }} # Dein Gitea-Benutzername
|
||||
repo: 'NoteVault' # Dein Repository-Name
|
||||
tag_name: 'v${{ github.sha }}' # Der Tag für die Version
|
||||
file: 'app/build/outputs/apk/release/app-release.apk' # Pfad zur APK-Datei
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -16,6 +16,3 @@ local.properties
|
||||
/music_database
|
||||
/music_database-shm
|
||||
/music_database-wal
|
||||
/note_database
|
||||
/note_database-shm
|
||||
/note_database-wal
|
||||
|
1
.idea/.gitignore
vendored
1
.idea/.gitignore
vendored
@ -6,4 +6,3 @@
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
/AndroidProjectSystem.xml
|
||||
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
@ -1,10 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="music_database" uuid="95a3c2ec-2c29-4336-900a-3993de90ae66">
|
||||
<data-source source="LOCAL" name="music_database" uuid="eb23f694-6586-450b-8f6f-a75731d36b96">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$USER_HOME$/.cache/JetBrains/IntelliJIdea2025.1/device-explorer/samsung SM-P610/_/data/data/com.stormtales.notevault/databases/music_database</jdbc-url>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/music_database</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
</jdbc-additional-properties>
|
||||
@ -19,53 +19,5 @@
|
||||
</library>
|
||||
</libraries>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="note_database" uuid="b3770d7c-0a73-40c6-aab8-010effaa19b6">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$USER_HOME$/.cache/JetBrains/IntelliJIdea2025.1/device-explorer/samsung SM-P610/_/data/data/come.stormborntales.notevault/databases/note_database</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
<libraries>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
|
||||
</library>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
|
||||
</library>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
|
||||
</library>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
|
||||
</library>
|
||||
</libraries>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="note_database [2]" uuid="ad94cfd9-e485-4151-8bf4-a080c51fa27c">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/note_database</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
<libraries>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
|
||||
</library>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
|
||||
</library>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
|
||||
</library>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
|
||||
</library>
|
||||
</libraries>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
@ -4,7 +4,7 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-05-03T08:34:29.354537334Z">
|
||||
<DropdownSelection timestamp="2025-04-10T18:07:32.446414465Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=R52N50NLGRT" />
|
||||
|
@ -5,6 +5,7 @@
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
@ -1,50 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.0.0" />
|
||||
</component>
|
||||
</project>
|
@ -3,5 +3,10 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
@ -1,17 +1,14 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.ksp) // NEU
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "come.stormborntales.notevault"
|
||||
namespace = "com.stormtales.notevault"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "come.stormborntales.notevault"
|
||||
minSdk = 24
|
||||
applicationId = "com.stormtales.notevault"
|
||||
minSdk = 29
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
@ -29,38 +26,28 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
implementation(libs.androidx.foundation)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.constraintlayout)
|
||||
implementation(libs.lifecycle.livedata.ktx)
|
||||
implementation(libs.compose.runtime.livedata)
|
||||
implementation(libs.coil.compose)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.navigation.fragment)
|
||||
implementation(libs.navigation.ui)
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.annotation)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
annotationProcessor(libs.room.compiler)
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package com.stormtales.notevault;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
assertEquals("com.stormtales.notevault", appContext.getPackageName());
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package come.stormborntales.notevault
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("come.stormborntales.notevault", appContext.packageName)
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
@ -16,15 +17,18 @@
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.NoteVault">
|
||||
android:theme="@style/Theme.NoteVault.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".FullscreenImageViewerActivity" />
|
||||
|
||||
<activity android:name=".ui.sheetdisplay.NoteSheetDisplayActivity"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar">
|
||||
<!-- Intent Filter hinzufügen, wenn Activity vom Launcher oder externen Apps aufgerufen werden soll -->
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
Binary file not shown.
Before Width: | Height: | Size: 197 KiB |
90
app/src/main/java/com/stormtales/notevault/MainActivity.java
Normal file
90
app/src/main/java/com/stormtales/notevault/MainActivity.java
Normal file
@ -0,0 +1,90 @@
|
||||
package com.stormtales.notevault;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Menu;
|
||||
import android.widget.Toast;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.ui.AppBarConfiguration;
|
||||
import androidx.navigation.ui.NavigationUI;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.stormtales.notevault.databinding.ActivityMainBinding;
|
||||
import com.stormtales.notevault.network.APICallback;
|
||||
import com.stormtales.notevault.network.auth.AuthService;
|
||||
import com.stormtales.notevault.ui.login.LoginDialog;
|
||||
import com.stormtales.notevault.ui.login.LoginViewModel;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private AppBarConfiguration mAppBarConfiguration;
|
||||
private ActivityMainBinding binding;
|
||||
private LoginViewModel loginViewModel;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setSupportActionBar(binding.appBarMain.toolbar);
|
||||
binding.appBarMain.toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.auth_action) {
|
||||
showLoginDialog();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
DrawerLayout drawer = binding.drawerLayout;
|
||||
NavigationView navigationView = binding.navView;
|
||||
// Passing each menu ID as a set of Ids because each
|
||||
// menu should be considered as top level destinations.
|
||||
mAppBarConfiguration = new AppBarConfiguration.Builder(
|
||||
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow)
|
||||
.setOpenableLayout(drawer)
|
||||
.build();
|
||||
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
|
||||
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
|
||||
NavigationUI.setupWithNavController(navigationView, navController);
|
||||
|
||||
loginViewModel = new ViewModelProvider(this).get(LoginViewModel.class);
|
||||
loginViewModel.setAuthService(new AuthService(getApplicationContext()));
|
||||
loginViewModel.getIsLoggedIn().observe(this, isLoggedIn -> {
|
||||
this.invalidateOptionsMenu();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
|
||||
return NavigationUI.navigateUp(navController, mAppBarConfiguration)
|
||||
|| super.onSupportNavigateUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuItem loginItem = menu.findItem(R.id.auth_action);
|
||||
if (Boolean.TRUE.equals(loginViewModel.getIsLoggedIn().getValue())) {
|
||||
loginItem.setIcon(R.drawable.logout); // Setze das Logout-Symbol
|
||||
} else {
|
||||
loginItem.setIcon(R.drawable.login); // Setze das Login-Symbol
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
public void showLoginDialog() {
|
||||
LoginDialog loginDialog = new LoginDialog(loginViewModel);
|
||||
loginDialog.show(getSupportFragmentManager(), "login");
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.stormtales.notevault.data;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.room.Database;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.RoomDatabase;
|
||||
import androidx.room.TypeConverters;
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.dao.SongDao;
|
||||
import com.stormtales.notevault.data.sync.DateConverter;
|
||||
import com.stormtales.notevault.data.sync.SyncStatusConverter;
|
||||
|
||||
@Database(entities = {Song.class, NoteSheet.class}, version = 1, exportSchema = false)
|
||||
@TypeConverters({SyncStatusConverter.class, DateConverter.class})
|
||||
public abstract class MusicDatabase extends RoomDatabase {
|
||||
public abstract SongDao getSongTable();
|
||||
|
||||
private static volatile MusicDatabase INSTANCE;
|
||||
|
||||
public static MusicDatabase getDatabase(final Context context) {
|
||||
if (INSTANCE == null) {
|
||||
synchronized (MusicDatabase.class) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
|
||||
MusicDatabase.class, "music_database")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.stormtales.notevault.data.dao;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.*;
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.sync.SyncStatus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface SongDao {
|
||||
@Insert
|
||||
long insert(Song song);
|
||||
|
||||
@Query("SELECT * FROM song WHERE syncStatus != 1")
|
||||
List<Song> getAllSongs();
|
||||
|
||||
@Update
|
||||
void update(Song song);
|
||||
|
||||
@Insert
|
||||
void insert(List<NoteSheet> noteSheets);
|
||||
|
||||
@Delete
|
||||
void deleteNoteSheets(List<NoteSheet> noteSheets);
|
||||
|
||||
@Query("SELECT * FROM NoteSheet WHERE songID = :songID")
|
||||
List<NoteSheet> getNoteSheetsBySong(int songID);
|
||||
|
||||
@Query("SELECT localFileName FROM NoteSheet WHERE songID = :songID")
|
||||
List<String> getNoteSheetFilesBySongID(int songID);
|
||||
|
||||
@Query("SELECT * FROM Song WHERE syncStatus = :syncStatus")
|
||||
List<Song> getSongsBySyncStatus(SyncStatus syncStatus);
|
||||
|
||||
@Update
|
||||
void updateSongs(List<Song> songs);
|
||||
|
||||
@Query("SELECT * FROM Song WHERE localID IN (:localIDs)")
|
||||
List<Song> getSongsByLocalIDs(List<Integer> localIDs);
|
||||
|
||||
@Query("SELECT * FROM Song WHERE serverID IN (:serverIDs)")
|
||||
List<Song> getSongsByServerIDs(List<String> serverIDs);
|
||||
|
||||
@Query("DELETE FROM Song WHERE serverID IN (:serverIDs)")
|
||||
void deleteSongsByServerIDs(List<String> serverIDs);
|
||||
|
||||
@Query("DELETE FROM SONG WHERE localID IN (:localIDs)")
|
||||
void deleteSongsByLocalIDs(List<Integer> localIDs);
|
||||
|
||||
@Query("SELECT * FROM NoteSheet WHERE songID IN (:localSongIDs)")
|
||||
List<NoteSheet> getNoteSheetsBySongIDs(List<Integer> localSongIDs);
|
||||
|
||||
@Query("SELECT * FROM NoteSheet WHERE songID IN (:songIDs)")
|
||||
List<NoteSheet> getNoteSheetsByLocalFiles(List<Integer> songIDs);
|
||||
|
||||
@Update
|
||||
void updateNoteSheets(List<NoteSheet> uploadedNoteSheets);
|
||||
|
||||
@Query("SELECT localID FROM Song WHERE serverID IN (:serverIDs)")
|
||||
List<Integer> getLocalSongIDsByServerIDs(List<String> serverIDs);
|
||||
|
||||
@Query("SELECT * FROM NoteSheet WHERE serverFileName IN (:serverFileNames)")
|
||||
List<NoteSheet> getNoteSheetsByServerFileNames(List<String> serverFileNames);
|
||||
|
||||
@Query("SELECT * FROM NoteSheet WHERE syncStatus = :status")
|
||||
List<NoteSheet> getNoteSheetsBySyncStatus(SyncStatus status);
|
||||
|
||||
@Query("SELECT * FROM NoteSheet WHERE songID IN (:localSongIDs)")
|
||||
List<NoteSheet> getNoteSheetFilesBySongIDs(List<Integer> localSongIDs);
|
||||
|
||||
@Insert
|
||||
void insertSongs(List<Song> onlyRemoteExistingSongs);
|
||||
|
||||
@Delete
|
||||
void deleteSong(Song song);
|
||||
|
||||
@Query("DELETE FROM NoteSheet WHERE songID = :localID")
|
||||
void deleteNoteSheetsByLocalSongID(int localID);
|
||||
|
||||
@Update
|
||||
void updateNoteSheet(NoteSheet noteSheet);
|
||||
|
||||
@Query("SELECT * FROM NoteSheet WHERE serverFileName =:serverFileName")
|
||||
NoteSheet getNoteSheetByServerFileName(String serverFileName);
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package com.stormtales.notevault.data.entities;
|
||||
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.PrimaryKey;
|
||||
import com.stormtales.notevault.data.sync.SyncStatus;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
public class NoteSheet {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private int localID;
|
||||
private int songID;
|
||||
private String localFileName;
|
||||
private String serverFileName;
|
||||
private String hash;
|
||||
|
||||
private SyncStatus syncStatus;
|
||||
|
||||
@Ignore
|
||||
public NoteSheet(String localFileName, String hash) {
|
||||
this.localFileName = localFileName;
|
||||
this.hash = hash;
|
||||
this.syncStatus = SyncStatus.CREATED;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public NoteSheet(int songID, String localFileName, String serverFileName, String hash) {
|
||||
this.songID = songID;
|
||||
this.localFileName = localFileName;
|
||||
this.serverFileName = serverFileName;
|
||||
this.hash = hash;
|
||||
this.syncStatus = SyncStatus.REMOTE_MODIFIED;
|
||||
}
|
||||
|
||||
|
||||
public NoteSheet() {
|
||||
|
||||
}
|
||||
|
||||
public int getLocalID() {
|
||||
return localID;
|
||||
}
|
||||
|
||||
public void setLocalID(int localID) {
|
||||
this.localID = localID;
|
||||
}
|
||||
|
||||
public String getLocalFileName() {
|
||||
return localFileName;
|
||||
}
|
||||
|
||||
public void setLocalFileName(String localFileName) {
|
||||
this.localFileName = localFileName;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public void setHash(String hash) {
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
public String getServerFileName() {
|
||||
return serverFileName;
|
||||
}
|
||||
|
||||
public void setServerFileName(String serverFileName) {
|
||||
this.serverFileName = serverFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
NoteSheet noteSheet = (NoteSheet) o;
|
||||
return localID == noteSheet.localID && Objects.equals(localFileName, noteSheet.localFileName) && Objects.equals(hash, noteSheet.hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(localID, localFileName, hash);
|
||||
}
|
||||
|
||||
public int getSongID() {
|
||||
return songID;
|
||||
}
|
||||
|
||||
public void setSongID(int songID) {
|
||||
this.songID = songID;
|
||||
}
|
||||
|
||||
public SyncStatus getSyncStatus() {
|
||||
return syncStatus;
|
||||
}
|
||||
|
||||
public void setSyncStatus(SyncStatus syncStatus) {
|
||||
this.syncStatus = syncStatus;
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package com.stormtales.notevault.data.entities;
|
||||
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.PrimaryKey;
|
||||
import com.stormtales.notevault.data.sync.SyncStatus;
|
||||
import com.stormtales.notevault.network.sync.models.SongModel;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
public class Song {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private int localID;
|
||||
|
||||
private String serverID;
|
||||
private SyncStatus syncStatus;
|
||||
private LocalDateTime syncTime;
|
||||
|
||||
/**Meta Data of Song*/
|
||||
private String title;
|
||||
private String composer;
|
||||
private String genre;
|
||||
private int year;
|
||||
|
||||
@Ignore
|
||||
public Song(String title, String composer, String genre, int year) {
|
||||
this.title = title;
|
||||
this.composer = composer;
|
||||
this.genre = genre;
|
||||
this.year = year;
|
||||
this.syncStatus = SyncStatus.CREATED;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public Song(SongModel songModel) {
|
||||
this.serverID = songModel.getServerID();
|
||||
this.title = songModel.getTitle();
|
||||
this.composer = songModel.getComposer();
|
||||
this.genre = songModel.getGenre();
|
||||
this.year = songModel.getYear();
|
||||
this.syncStatus = SyncStatus.SYNCED;
|
||||
this.syncTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public Song() {
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public Song(String serverID) {
|
||||
this.serverID = serverID;
|
||||
this.syncStatus = SyncStatus.REMOTE_MODIFIED;
|
||||
}
|
||||
|
||||
public int getLocalID() {
|
||||
return localID;
|
||||
}
|
||||
|
||||
public void setLocalID(int localID) {
|
||||
this.localID = localID;
|
||||
}
|
||||
|
||||
public String getServerID() {
|
||||
return serverID;
|
||||
}
|
||||
|
||||
public void setServerID(String serverID) {
|
||||
this.serverID = serverID;
|
||||
}
|
||||
|
||||
public SyncStatus getSyncStatus() {
|
||||
return syncStatus;
|
||||
}
|
||||
|
||||
public void setSyncStatus(SyncStatus syncStatus) {
|
||||
this.syncStatus = syncStatus;
|
||||
}
|
||||
|
||||
public LocalDateTime getSyncTime() {
|
||||
return syncTime;
|
||||
}
|
||||
|
||||
public void setSyncTime(LocalDateTime syncTime) {
|
||||
this.syncTime = syncTime;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getComposer() {
|
||||
return composer;
|
||||
}
|
||||
|
||||
public void setComposer(String composer) {
|
||||
this.composer = composer;
|
||||
}
|
||||
|
||||
public String getGenre() {
|
||||
return genre;
|
||||
}
|
||||
|
||||
public void setGenre(String genre) {
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
public void setYear(int year) {
|
||||
this.year = year;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Song song = (Song) o;
|
||||
return localID == song.localID && Objects.equals(serverID, song.serverID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(localID, serverID);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.stormtales.notevault.data.entities;
|
||||
|
||||
import androidx.room.Embedded;
|
||||
import androidx.room.Relation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SongNoteSheet {
|
||||
@Embedded public Song song;
|
||||
@Relation(
|
||||
parentColumn = "localID",
|
||||
entityColumn = "songID"
|
||||
)
|
||||
public List<NoteSheet> noteSheets;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.stormtales.notevault.data.model;
|
||||
|
||||
/**
|
||||
* Data class that captures user information for logged in users retrieved from LoginRepository
|
||||
*/
|
||||
public class LoggedInUser {
|
||||
|
||||
private String userId;
|
||||
private String displayName;
|
||||
|
||||
public LoggedInUser(String userId, String displayName) {
|
||||
this.userId = userId;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package com.stormtales.notevault.data.repositories;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import com.stormtales.notevault.data.MusicDatabase;
|
||||
import com.stormtales.notevault.data.dao.SongDao;
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.sync.SyncStatus;
|
||||
import com.stormtales.notevault.utils.NoteSheetsUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class SongRepository {
|
||||
private SongDao songDao;
|
||||
public SongRepository(Context context) {
|
||||
MusicDatabase database = MusicDatabase.getDatabase(context);
|
||||
songDao = database.getSongTable();
|
||||
}
|
||||
public void insert(Song song, Callback<Long> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
long result = songDao.insert(song);
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(result));
|
||||
});
|
||||
}
|
||||
|
||||
public interface LoadHomeViewModelCallback<T> {
|
||||
void onResult(T result);
|
||||
}
|
||||
|
||||
public void getAllSongs(LoadHomeViewModelCallback<List<Song>> callback) {
|
||||
ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
executorService.execute(() -> {
|
||||
List<Song> songs = songDao.getAllSongs();
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(songs));
|
||||
});
|
||||
}
|
||||
|
||||
public void updateSong(Song song) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
songDao.update(song);
|
||||
});
|
||||
}
|
||||
|
||||
public void insertNoteSheets(List<NoteSheet> noteSheets) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
songDao.insert(noteSheets);
|
||||
});
|
||||
}
|
||||
|
||||
public interface Callback<T> {
|
||||
void onResult(T result);
|
||||
}
|
||||
|
||||
public void deleteSong(Song song) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
song.setSyncStatus(SyncStatus.DELETED);
|
||||
songDao.update(song);
|
||||
|
||||
List<NoteSheet> noteSheets = songDao.getNoteSheetsBySong(song.getLocalID());
|
||||
for(NoteSheet noteSheet : noteSheets) {
|
||||
try {
|
||||
NoteSheetsUtil.deleteNoteSheet(noteSheet.getLocalFileName());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
songDao.deleteNoteSheets(noteSheets);
|
||||
});
|
||||
}
|
||||
|
||||
public void getNoteSheetFilesBySongID(int songID, Callback<List<String>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(()-> {
|
||||
List<String> noteSheetFiles = songDao.getNoteSheetFilesBySongID(songID);
|
||||
callback.onResult(noteSheetFiles);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,303 @@
|
||||
package com.stormtales.notevault.data.repositories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.recyclerview.widget.AsyncListUtil;
|
||||
import com.stormtales.notevault.data.MusicDatabase;
|
||||
import com.stormtales.notevault.data.dao.SongDao;
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.sync.SyncStatus;
|
||||
import com.stormtales.notevault.network.sync.models.*;
|
||||
import com.stormtales.notevault.utils.NoteSheetsUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SongSyncRepository {
|
||||
private SongDao songDao;
|
||||
public SongSyncRepository(Context context) {
|
||||
MusicDatabase database = MusicDatabase.getDatabase(context);
|
||||
songDao = database.getSongTable();
|
||||
}
|
||||
|
||||
public void loadCreatedSongs(LoadDataCallback<List<Song>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<Song> createdSongs = songDao.getSongsBySyncStatus(SyncStatus.CREATED);
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(createdSongs));
|
||||
});
|
||||
}
|
||||
|
||||
public void loadModifiedSongs(LoadDataCallback<Map<Song, List<NoteSheet>>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
Map<Song, List<NoteSheet>> result = new HashMap<>();
|
||||
List<Song> modifiedSongs = songDao.getSongsBySyncStatus(SyncStatus.MODIFIED);
|
||||
for(Song song : modifiedSongs) {
|
||||
List<NoteSheet> noteSheets = songDao.getNoteSheetsBySong(song.getLocalID());
|
||||
result.put(song, noteSheets);
|
||||
}
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(result));
|
||||
});
|
||||
}
|
||||
|
||||
public void loadDeletedSongs(LoadDataCallback<List<Song>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<Song> deletedSongs = songDao.getSongsBySyncStatus(SyncStatus.DELETED);
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(deletedSongs));
|
||||
});
|
||||
}
|
||||
|
||||
public void markCreatedSongsAsSynced(BatchCreateResponse createdSongs) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<Integer> localIDs = createdSongs.getCreateResponses().stream().map(CreateResponse::getLocalID).collect(Collectors.toList());
|
||||
List<Song> requestedSongs = songDao.getSongsByLocalIDs(localIDs);
|
||||
|
||||
for(Song song : requestedSongs) {
|
||||
song.setSyncTime(LocalDateTime.now());
|
||||
song.setSyncStatus(SyncStatus.SYNCED);
|
||||
|
||||
for(CreateResponse createResponse : createdSongs.getCreateResponses()) {
|
||||
if(createResponse.getLocalID() == song.getLocalID()) {
|
||||
song.setServerID(createResponse.getServerID());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
songDao.updateSongs(requestedSongs);
|
||||
});
|
||||
}
|
||||
|
||||
public void markSongsAsSynced(List<Song> songs) {
|
||||
for(Song song : songs) {
|
||||
song.setSyncStatus(SyncStatus.SYNCED);
|
||||
}
|
||||
}
|
||||
|
||||
public void markModifiedSongsAsSynced(SongModifyBatchResponse modifyBatchResponse) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<String> serverIDs = modifyBatchResponse.getModifiedServerObjects().stream().map(SongModifyResponse::getServerID).collect(Collectors.toList());
|
||||
List<Song> requestedSongs = songDao.getSongsByServerIDs(serverIDs);
|
||||
for(Song song : requestedSongs) {
|
||||
song.setSyncStatus(SyncStatus.SYNCED);
|
||||
song.setSyncTime(LocalDateTime.now());
|
||||
|
||||
|
||||
for(SongModifyResponse modifyResponse : modifyBatchResponse.getModifiedServerObjects()) {
|
||||
if(modifyResponse.getServerID().equals(song.getServerID())) {
|
||||
List<NoteSheet> outdatedNoteSheets = songDao.getNoteSheetsByServerFileNames(modifyResponse.getOutdated_note_sheets());
|
||||
for(NoteSheet noteSheet : outdatedNoteSheets) {
|
||||
noteSheet.setSyncStatus(SyncStatus.MODIFIED);
|
||||
}
|
||||
songDao.updateNoteSheets(outdatedNoteSheets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
songDao.updateSongs(requestedSongs);
|
||||
});
|
||||
}
|
||||
|
||||
public void markDeletedSongsAsSynced(List<String> remoteDeletedSongs, List<Integer> localDeletedSongs) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
songDao.deleteSongsByServerIDs(remoteDeletedSongs);
|
||||
songDao.deleteSongsByLocalIDs(localDeletedSongs);
|
||||
});
|
||||
}
|
||||
|
||||
public void getNoteSheetFilesBySongIDs(LoadDataCallback<List<NoteSheet>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<NoteSheet> noteSheets = songDao.getNoteSheetsBySyncStatus(SyncStatus.CREATED);
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(noteSheets));
|
||||
});
|
||||
}
|
||||
|
||||
public void markCreatedNoteSheetsAsSynced(List<UploadResponse> uploadResponses) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<String> serverSongIDs = uploadResponses.stream().map(UploadResponse::getServerID).collect(Collectors.toList());
|
||||
List<Integer> localSongIDs = songDao.getLocalSongIDsByServerIDs(serverSongIDs);
|
||||
|
||||
List<NoteSheet> uploadedNoteSheets = songDao.getNoteSheetsBySongIDs(localSongIDs);
|
||||
for(UploadResponse uploadResponse : uploadResponses) {
|
||||
for(NoteSheet noteSheet : uploadedNoteSheets) {
|
||||
if(new File(noteSheet.getLocalFileName()).getName().equals(uploadResponse.getLocalFile())) {
|
||||
noteSheet.setServerFileName(uploadResponse.getServerFile());
|
||||
noteSheet.setSyncStatus(SyncStatus.SYNCED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
songDao.updateNoteSheets(uploadedNoteSheets);
|
||||
});
|
||||
}
|
||||
|
||||
public void loadModifiedNoteSheets(LoadDataCallback<List<NoteSheet>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<NoteSheet> noteSheets = songDao.getNoteSheetsBySyncStatus(SyncStatus.MODIFIED);
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(noteSheets));
|
||||
});
|
||||
}
|
||||
|
||||
public void markModifiedNoteSheetsAsSynced(List<UploadResponse> uploadResponses) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<String> serverFileNames = uploadResponses.stream().map(UploadResponse::getServerFile).collect(Collectors.toList());
|
||||
|
||||
|
||||
List<NoteSheet> noteSheets = songDao.getNoteSheetsByServerFileNames(serverFileNames);
|
||||
for(NoteSheet noteSheet : noteSheets) {
|
||||
noteSheet.setSyncStatus(SyncStatus.SYNCED);
|
||||
}
|
||||
songDao.updateNoteSheets(noteSheets);
|
||||
});
|
||||
}
|
||||
|
||||
public void markSongsAsRemotelyModified(List<String> serverIDs, LoadDataCallback<List<Song>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<Song> songs = songDao.getSongsByServerIDs(serverIDs);
|
||||
List<Song> createdSongs = new ArrayList<>();
|
||||
for(Song song : songs) {
|
||||
song.setSyncStatus(SyncStatus.REMOTE_MODIFIED);
|
||||
}
|
||||
|
||||
for(String serverID : serverIDs) {
|
||||
boolean foundServerSong = false;
|
||||
for(Song song : songs) {
|
||||
if(song.getServerID().equals(serverID)) {
|
||||
foundServerSong = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!foundServerSong) {
|
||||
Song song = new Song(serverID);
|
||||
createdSongs.add(song);
|
||||
}
|
||||
}
|
||||
songDao.updateSongs(songs);
|
||||
songDao.insertSongs(createdSongs);
|
||||
callback.onResult(createdSongs);
|
||||
});
|
||||
}
|
||||
|
||||
public void loadRemotelyModifiedSongs(LoadDataCallback<List<Song>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
List<Song> songs = songDao.getSongsBySyncStatus(SyncStatus.REMOTE_MODIFIED);
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(songs));
|
||||
});
|
||||
}
|
||||
|
||||
public void saveRemoteSongs(List<SongModel> remoteSongs, LoadDataCallback<List<NoteSheet>> callback) {
|
||||
Executors.newSingleThreadExecutor().execute(() ->{
|
||||
List<String> serverIDs = remoteSongs.stream().map(SongModel::getServerID).collect(Collectors.toList());
|
||||
List<Song> songs = songDao.getSongsByServerIDs(serverIDs);
|
||||
List<Integer> localSongIDs = songDao.getLocalSongIDsByServerIDs(serverIDs);
|
||||
List<NoteSheet> noteSheets = songDao.getNoteSheetsBySongIDs(localSongIDs);
|
||||
|
||||
List<Song> onlyRemoteExistingSongs = new ArrayList<>();
|
||||
List<NoteSheet> outdatedNoteSheets = new ArrayList<>();
|
||||
List<NoteSheet> onlyRemoteNoteSheets = new ArrayList<>();
|
||||
for(SongModel songModel : remoteSongs) {
|
||||
boolean found = false;
|
||||
for(Song song : songs) {
|
||||
if(song.getServerID().equals(songModel.getServerID())) {
|
||||
found = true;
|
||||
if(!songModel.isDeleted()) {
|
||||
song.setSyncStatus(SyncStatus.SYNCED);
|
||||
song.setSyncTime(LocalDateTime.now());
|
||||
|
||||
song.setTitle(songModel.getTitle());
|
||||
song.setComposer(songModel.getComposer());
|
||||
song.setGenre(songModel.getGenre());
|
||||
song.setYear(songModel.getYear());
|
||||
|
||||
for(RemotelyModifiedNoteSheetModel remoteNoteSheet : songModel.getNote_sheets()) {
|
||||
if(noteSheets.isEmpty()) {
|
||||
NoteSheet noteSheet = new NoteSheet(song.getLocalID(), remoteNoteSheet.getFilename(), remoteNoteSheet.getServer_filename(), null);
|
||||
onlyRemoteNoteSheets.add(noteSheet);
|
||||
outdatedNoteSheets.add(noteSheet);
|
||||
} else {
|
||||
boolean foundNoteSheet = false;
|
||||
for(NoteSheet noteSheet : noteSheets) {
|
||||
if(remoteNoteSheet.getServer_filename().equals(noteSheet.getServerFileName())) {
|
||||
foundNoteSheet = true;
|
||||
if(!remoteNoteSheet.getHash().equals(noteSheet.getHash())) {
|
||||
outdatedNoteSheets.add(noteSheet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!foundNoteSheet) {
|
||||
NoteSheet noteSheet = new NoteSheet(song.getLocalID(), remoteNoteSheet.getFilename(), remoteNoteSheet.getServer_filename(), null);
|
||||
onlyRemoteNoteSheets.add(noteSheet);
|
||||
outdatedNoteSheets.add(noteSheet);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
removeSong(song);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!found) {
|
||||
Song song = new Song(songModel);
|
||||
onlyRemoteExistingSongs.add(song);
|
||||
}
|
||||
}
|
||||
|
||||
songDao.updateSongs(songs);
|
||||
songDao.insertSongs(onlyRemoteExistingSongs);
|
||||
songDao.insert(onlyRemoteNoteSheets);
|
||||
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(()-> callback.onResult(outdatedNoteSheets));
|
||||
});
|
||||
}
|
||||
|
||||
private void removeSong(Song song) {
|
||||
List<NoteSheet> noteSheets = songDao.getNoteSheetsBySong(song.getLocalID());
|
||||
for(NoteSheet noteSheet : noteSheets) {
|
||||
try {
|
||||
NoteSheetsUtil.deleteNoteSheet(noteSheet.getLocalFileName());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
songDao.deleteNoteSheets(noteSheets);
|
||||
songDao.deleteSong(song);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void saveUpdatedNoteSheet(String serverFileName, String localFileName, String hash) {
|
||||
Executors.newSingleThreadExecutor().execute(()-> {
|
||||
NoteSheet noteSheet = songDao.getNoteSheetByServerFileName(serverFileName);
|
||||
noteSheet.setHash(hash);
|
||||
noteSheet.setLocalFileName(localFileName);
|
||||
songDao.updateNoteSheet(noteSheet);
|
||||
});
|
||||
}
|
||||
|
||||
public interface LoadDataCallback<T> {
|
||||
void onResult(T result);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.stormtales.notevault.data.sync;
|
||||
|
||||
import androidx.room.TypeConverter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class DateConverter {
|
||||
// Konvertiere LocalDateTime in String
|
||||
@TypeConverter
|
||||
public static String fromLocalDateTime(LocalDateTime localDateTime) {
|
||||
return localDateTime == null ? null : localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
}
|
||||
|
||||
// Konvertiere String zurück in LocalDateTime
|
||||
@TypeConverter
|
||||
public static LocalDateTime toLocalDateTime(String dateTimeString) {
|
||||
return dateTimeString == null ? null : LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.stormtales.notevault.data.sync;
|
||||
|
||||
public enum SyncStatus {
|
||||
CREATED,
|
||||
DELETED,
|
||||
MODIFIED,
|
||||
REMOTE_MODIFIED,
|
||||
SYNCED;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.stormtales.notevault.data.sync;
|
||||
|
||||
import androidx.room.TypeConverter;
|
||||
|
||||
public class SyncStatusConverter {
|
||||
@TypeConverter
|
||||
public static SyncStatus fromInt(int value) {
|
||||
return SyncStatus.values()[value];
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static int toInt(SyncStatus syncStatus) {
|
||||
return syncStatus.ordinal();
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.stormtales.notevault.network;
|
||||
|
||||
public interface APICallback {
|
||||
void onSuccess();
|
||||
void onError(String error);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.stormtales.notevault.network;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import com.stormtales.notevault.network.auth.AuthInterceptor;
|
||||
import com.stormtales.notevault.network.auth.TokenManager;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
public class NetworkModule {
|
||||
private static final String BASE_URL = "https://notevault.fawkes100.de/";
|
||||
private static Retrofit retrofit;
|
||||
|
||||
public static Retrofit getRetrofitInstance(Context context) {
|
||||
if (retrofit == null) {
|
||||
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
|
||||
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
TokenManager tokenManager = new TokenManager(context);
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new AuthInterceptor(tokenManager))
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build();
|
||||
|
||||
retrofit = new Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build();
|
||||
}
|
||||
return retrofit;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.stormtales.notevault.network;
|
||||
|
||||
public class StatusResponse {
|
||||
|
||||
private String status;
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.stormtales.notevault.network.auth;
|
||||
|
||||
import com.stormtales.notevault.network.StatusResponse;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
public interface AuthAPI {
|
||||
|
||||
@POST("/login/")
|
||||
Call<LoginResponse> login(@Body LoginRequest loginRequest);
|
||||
|
||||
@POST("/register/")
|
||||
@Headers("Content-Type: application/json")
|
||||
Call<StatusResponse> registration(@Body RegisterRequest registerRequest);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.stormtales.notevault.network.auth;
|
||||
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class AuthInterceptor implements Interceptor {
|
||||
private TokenManager tokenManager;
|
||||
|
||||
public AuthInterceptor(TokenManager tokenManager) {
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request originalRequest = chain.request();
|
||||
|
||||
String token = tokenManager.getToken();
|
||||
if (token == null) {
|
||||
return chain.proceed(originalRequest);
|
||||
}
|
||||
|
||||
Request authenticatedRequest = originalRequest.newBuilder()
|
||||
.header("Authorization", "Bearer " + token).build();
|
||||
return chain.proceed(authenticatedRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.stormtales.notevault.network.auth;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import com.stormtales.notevault.network.APICallback;
|
||||
import com.stormtales.notevault.network.NetworkModule;
|
||||
import com.stormtales.notevault.network.StatusResponse;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class AuthService {
|
||||
|
||||
private final AuthAPI authAPI;
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
public AuthService(Context context) {
|
||||
this.authAPI = NetworkModule.getRetrofitInstance(context).create(AuthAPI.class);
|
||||
this.tokenManager = new TokenManager(context);
|
||||
}
|
||||
|
||||
public void performLogin(String email, String password, LoginCallback callback) {
|
||||
LoginRequest loginRequest = new LoginRequest(email, password);
|
||||
|
||||
authAPI.login(loginRequest).enqueue(new Callback<LoginResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<LoginResponse> call, Response<LoginResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
String token = response.body().getToken();
|
||||
saveToken(token);
|
||||
// Erfolgsrückmeldung an den Callback senden
|
||||
callback.onSuccess(response.body());
|
||||
} else {
|
||||
// Fehlermeldung an den Callback senden
|
||||
callback.onError("Login fehlgeschlagen. Überprüfe Benutzername und Passwort.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<LoginResponse> call, Throwable t) {
|
||||
// Netzwerkfehler an den Callback senden
|
||||
callback.onError("Netzwerkfehler: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void performRegistration(String email, String username, String password, APICallback callback) {
|
||||
RegisterRequest registerRequest = new RegisterRequest(username, password, email);
|
||||
authAPI.registration(registerRequest).enqueue(new Callback<StatusResponse>() {
|
||||
|
||||
@Override
|
||||
public void onResponse(Call<StatusResponse> call, Response<StatusResponse> response) {
|
||||
if(response.isSuccessful() && response.body() != null) {
|
||||
callback.onSuccess();
|
||||
} else {
|
||||
callback.onError("Registration fehlgeschlagen. Überprüfe Benutzername und Passwort.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<StatusResponse> call, Throwable throwable) {
|
||||
callback.onError("Netzwerkfehler: " + throwable.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void saveToken(String token) {
|
||||
tokenManager.saveToken(token);
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return tokenManager.getToken();
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
tokenManager.clearToken();
|
||||
}
|
||||
|
||||
public boolean isLoggedIn() {
|
||||
return !TextUtils.isEmpty(tokenManager.getToken());
|
||||
}
|
||||
|
||||
public interface LoginCallback {
|
||||
void onSuccess(LoginResponse loginResponse);
|
||||
void onError(String error);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.stormtales.notevault.network.auth;
|
||||
|
||||
public class LoginRequest {
|
||||
private String email;
|
||||
private String password;
|
||||
|
||||
public LoginRequest(String email, String password) {
|
||||
this.email = email;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.stormtales.notevault.network.auth;
|
||||
|
||||
public class LoginResponse {
|
||||
private String token;
|
||||
private String username;
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.stormtales.notevault.network.auth;
|
||||
|
||||
public class RegisterRequest {
|
||||
private String username;
|
||||
private String email;
|
||||
private String password;
|
||||
|
||||
public RegisterRequest(String username, String password, String email) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.stormtales.notevault.network.auth;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class TokenManager {
|
||||
private static final String PREF_NAME = "app_preferences";
|
||||
private static final String KEY_TOKEN = "jwt_token";
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
public TokenManager(Context context) {
|
||||
sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public void saveToken(String token) {
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
editor.putString(KEY_TOKEN, token);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return sharedPreferences.getString(KEY_TOKEN, null);
|
||||
}
|
||||
|
||||
public void clearToken() {
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
editor.remove(KEY_TOKEN);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package com.stormtales.notevault.network.sync;
|
||||
|
||||
import com.stormtales.notevault.network.auth.LoginRequest;
|
||||
import com.stormtales.notevault.network.auth.LoginResponse;
|
||||
import com.stormtales.notevault.network.sync.models.*;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public interface SongSyncAPI {
|
||||
|
||||
@POST("/sync/songs/create")
|
||||
Call<BatchCreateResponse> syncCreatedSongs(@Body SongCreateBatchRequest songCreateBatchRequest);
|
||||
|
||||
@POST("/sync/songs/modify")
|
||||
Call<SongModifyBatchResponse> syncModifiedSongs(@Body SongModifyBatchRequest songModifyBatchRequest);
|
||||
|
||||
@POST("/sync/songs/delete")
|
||||
Call<BatchModifyResponse> syncDeletedSongs(@Body SongBatchDeleteRequest songBatchDeleteRequest);
|
||||
|
||||
@Multipart
|
||||
@POST("/sync/songs/note_sheet/upload")
|
||||
Call<UploadResponse> uploadNoteSheet(@Part("serverID")RequestBody serverID, @Part("fileName") RequestBody fileName, @Part MultipartBody.Part image);
|
||||
|
||||
@Multipart
|
||||
@POST("/sync/songs/note_sheet/update")
|
||||
Call<UploadResponse> updateNoteSheet(@Part("server_filename") RequestBody server_filename, @Part MultipartBody.Part image);
|
||||
|
||||
@GET("/sync/songs/fetch")
|
||||
Call<FetchResponse> fetchRemoteModifiedSongs(@Query(value = "last_client_sync") String last_client_sync);
|
||||
|
||||
@GET("/sync/songs/get/")
|
||||
Call<SongModel> fetchRemotelyModifiedSongData(@Query(value = "songID") String songID);
|
||||
|
||||
@GET("/sync/songs/get/notesheet/")
|
||||
Call<ResponseBody> downloadNotesheet(@Query("server_filename") String server_filename);
|
||||
|
||||
@Multipart
|
||||
@POST("/ai/regognize/title")
|
||||
Call<AIRecognizedSong> recognizeTitle(@Part MultipartBody.Part image);
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
package com.stormtales.notevault.network.sync;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.repositories.SongRepository;
|
||||
import com.stormtales.notevault.data.repositories.SongSyncRepository;
|
||||
import com.stormtales.notevault.network.sync.models.*;
|
||||
import com.stormtales.notevault.ui.gallery.GalleryViewModel;
|
||||
import com.stormtales.notevault.utils.NoteSheetsUtil;
|
||||
import com.stormtales.notevault.utils.Tupel;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SongSyncModule {
|
||||
private SongRepository songRepository;
|
||||
private SongSyncRepository songSyncRepository;
|
||||
private SongSyncService songSyncService;
|
||||
private GalleryViewModel syncViewModel;
|
||||
private Context context;
|
||||
|
||||
public SongSyncModule(Context context, GalleryViewModel syncViewModel) {
|
||||
this.songRepository = new SongRepository(context);
|
||||
this.songSyncRepository = new SongSyncRepository(context);
|
||||
this.songSyncService = new SongSyncService(context);
|
||||
this.syncViewModel = syncViewModel;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void syncCreatedSongs() {
|
||||
songSyncRepository.loadCreatedSongs(result -> {
|
||||
songSyncService.syncCreatedSongs(result, response -> {
|
||||
songSyncRepository.markCreatedSongsAsSynced(response);
|
||||
if(response.getCreateResponses().isEmpty()) {
|
||||
syncViewModel.finishCreateSongSyncing();
|
||||
} else {
|
||||
uploadCreatedNoteSheets(result, response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void uploadCreatedNoteSheets(List<Song> result, BatchCreateResponse response) {
|
||||
List<Integer> songIDs = result.stream().map(Song::getLocalID).collect(Collectors.toList());
|
||||
songSyncRepository.getNoteSheetFilesBySongIDs(noteSheets -> {
|
||||
songSyncService.uploadNoteSheetsOfCreatedSongs(noteSheets, response, uploadResponses -> {
|
||||
songSyncRepository.markCreatedNoteSheetsAsSynced(uploadResponses);
|
||||
syncViewModel.finishCreateSongSyncing();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void syncModifiedSongs() {
|
||||
songSyncRepository.loadModifiedSongs(result -> {
|
||||
songSyncService.syncModifiedSongs(result, (FinishSongSyncingCallback<SongModifyBatchResponse>) response -> {
|
||||
songSyncRepository.markModifiedSongsAsSynced(response);
|
||||
uploadModifiedNoteSheets();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void uploadModifiedNoteSheets() {
|
||||
songSyncRepository.loadModifiedNoteSheets(modifiedNoteSheets -> {
|
||||
if(modifiedNoteSheets.isEmpty()) {
|
||||
syncViewModel.finishModifiedSongSyncinc();
|
||||
} else {
|
||||
songSyncService.uploadModifiedNoteSheets(modifiedNoteSheets, new SongSyncService.UploadNoteSheetCallback() {
|
||||
@Override
|
||||
public void finishUploadNoteSheets(List<UploadResponse> uploadResponses) {
|
||||
songSyncRepository.markModifiedNoteSheetsAsSynced(uploadResponses);
|
||||
syncViewModel.finishModifiedSongSyncinc();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void syncDeletedSongs() {
|
||||
songSyncRepository.loadDeletedSongs(result -> {
|
||||
songSyncService.syncDeletedSong(result, (remoteDeletedSongs, localDeletedSongs) -> {
|
||||
songSyncRepository.markDeletedSongsAsSynced(remoteDeletedSongs, localDeletedSongs);
|
||||
syncViewModel.finishDeleteSongSyncinc();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void fetchRemoteModifiedSongs() {
|
||||
//Todo: Determine Last Client Sync; for testing use LocalDateTime.MIN
|
||||
songSyncService.fetchRemoteModifiedSongs(LocalDateTime.now().minusDays(35), new SongSyncRepository.LoadDataCallback<FetchResponse>() {
|
||||
@Override
|
||||
public void onResult(FetchResponse result) {
|
||||
songSyncRepository.markSongsAsRemotelyModified(result.getServerIDs(), createdSongs -> {
|
||||
getRemotelyModifiedSongData(createdSongs);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void getRemotelyModifiedSongData(List<Song> freshlyCreatedSongs) {
|
||||
songSyncRepository.loadRemotelyModifiedSongs(songs -> {
|
||||
for(Song song : freshlyCreatedSongs) {
|
||||
boolean freshlyCreatedSongFound = false;
|
||||
for(Song s : songs) {
|
||||
if(s.getServerID().equals(song.getServerID())) {
|
||||
freshlyCreatedSongFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!freshlyCreatedSongFound) {
|
||||
songs.add(song);
|
||||
}
|
||||
|
||||
}
|
||||
if(songs.isEmpty()) {
|
||||
syncViewModel.finishFetching();
|
||||
} else {
|
||||
songSyncService.getRemotelyModifiedSongData(songs, remoteSongs -> {
|
||||
if(remoteSongs.isEmpty()) {
|
||||
syncViewModel.finishFetching();
|
||||
} else {
|
||||
songSyncRepository.saveRemoteSongs(remoteSongs, this::downloadNoteSheets);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public void downloadNoteSheets(List<NoteSheet> noteSheets) {
|
||||
for(NoteSheet noteSheet : noteSheets) {
|
||||
songSyncService.downloadNoteSheet(noteSheet, responseBody -> {
|
||||
String localFilename = noteSheet.getServerFileName().substring(36);
|
||||
|
||||
Tupel<String, String> result = NoteSheetsUtil.saveImageFromServer(responseBody, context.getFilesDir());
|
||||
songSyncRepository.saveUpdatedNoteSheet(noteSheet.getServerFileName(), result.getValue00(), result.getValue01());
|
||||
});
|
||||
}
|
||||
syncViewModel.finishFetching();
|
||||
}
|
||||
|
||||
public interface FinishSongCreateSyncingCallback {
|
||||
void finishSongSyncing(BatchCreateResponse response);
|
||||
}
|
||||
|
||||
public interface FinishSongSyncingCallback<T> {
|
||||
void finishSongSyncing(T response);
|
||||
}
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
package com.stormtales.notevault.network.sync;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.repositories.SongSyncRepository;
|
||||
import com.stormtales.notevault.network.NetworkModule;
|
||||
import com.stormtales.notevault.network.auth.AuthAPI;
|
||||
import com.stormtales.notevault.network.sync.models.*;
|
||||
import com.stormtales.notevault.ui.gallery.GalleryViewModel;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SongSyncService {
|
||||
private final SongSyncAPI songSyncAPI;
|
||||
private GalleryViewModel syncStatusViewModel;
|
||||
|
||||
public SongSyncService(Context context) {
|
||||
songSyncAPI = NetworkModule.getRetrofitInstance(context).create(SongSyncAPI.class);
|
||||
}
|
||||
|
||||
public void syncCreatedSongs(List<Song> songs, SongSyncModule.FinishSongCreateSyncingCallback callback) {
|
||||
List<SongCreateRequest> createRequests = new ArrayList<>();
|
||||
for(Song song : songs) {
|
||||
createRequests.add(new SongCreateRequest(song));
|
||||
}
|
||||
SongCreateBatchRequest songCreateBatchRequest = new SongCreateBatchRequest(createRequests);
|
||||
songSyncAPI.syncCreatedSongs(songCreateBatchRequest).enqueue(new Callback<BatchCreateResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<BatchCreateResponse> call, Response<BatchCreateResponse> response) {
|
||||
callback.finishSongSyncing(response.body());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<BatchCreateResponse> call, Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void uploadNoteSheetsOfCreatedSongs(List<NoteSheet> noteSheets, BatchCreateResponse batchCreateResponse, UploadNoteSheetCallback callback) {
|
||||
List<UploadResponse> uploadResponses = new ArrayList<>();
|
||||
for(CreateResponse createResponse : batchCreateResponse.getCreateResponses()) {
|
||||
for(NoteSheet noteSheet : noteSheets) {
|
||||
if(noteSheet.getSongID() == createResponse.getLocalID()) {
|
||||
File imageFile = new File(noteSheet.getLocalFileName());
|
||||
|
||||
RequestBody serverID = RequestBody.create(MediaType.parse("text/plain"), createResponse.getServerID());
|
||||
RequestBody fileName = RequestBody.create(MediaType.parse("text/plain"), imageFile.getName());
|
||||
|
||||
RequestBody requestFile = RequestBody.create(MediaType.parse("image/**"), new File(noteSheet.getLocalFileName()));
|
||||
MultipartBody.Part image = MultipartBody.Part.createFormData("image", noteSheet.getLocalFileName(), requestFile);
|
||||
|
||||
songSyncAPI.uploadNoteSheet(serverID, fileName, image).enqueue(new Callback<UploadResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<UploadResponse> call, Response<UploadResponse> response) {
|
||||
if(response.isSuccessful() && response.body() != null) {
|
||||
uploadResponses.add(response.body());
|
||||
if(uploadResponses.size() == noteSheets.size()) {
|
||||
callback.finishUploadNoteSheets(uploadResponses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<UploadResponse> call, Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void syncModifiedSongs(Map<Song, List<NoteSheet>> songs, SongSyncModule.FinishSongSyncingCallback<SongModifyBatchResponse> callback) {
|
||||
List<SongModifyRequest> modifyRequests = new ArrayList<>();
|
||||
for(Map.Entry<Song, List<NoteSheet>> entry: songs.entrySet()) {
|
||||
modifyRequests.add(new SongModifyRequest(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
SongModifyBatchRequest songModifyBatchRequest = new SongModifyBatchRequest(modifyRequests);
|
||||
songSyncAPI.syncModifiedSongs(songModifyBatchRequest).enqueue(new Callback<SongModifyBatchResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<SongModifyBatchResponse> call, Response<SongModifyBatchResponse> response) {
|
||||
callback.finishSongSyncing(response.body());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<SongModifyBatchResponse> call, Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void syncDeletedSong(List<Song> songs, SyncDeletedSongsCallback callback) {
|
||||
List<String> deleteRequests = new ArrayList<>();
|
||||
List<Integer> deletedNotSyncedSongs = new ArrayList<>();
|
||||
for(Song song : songs) {
|
||||
if(song.getServerID() == null) {
|
||||
deletedNotSyncedSongs.add(song.getLocalID());
|
||||
} else {
|
||||
deleteRequests.add(song.getServerID());
|
||||
}
|
||||
}
|
||||
|
||||
SongBatchDeleteRequest songBatchDeleteRequest = new SongBatchDeleteRequest(deleteRequests);
|
||||
songSyncAPI.syncDeletedSongs(songBatchDeleteRequest).enqueue(new Callback<BatchModifyResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<BatchModifyResponse> call, Response<BatchModifyResponse> response) {
|
||||
if(response.isSuccessful() && response.body() != null) {
|
||||
callback.finishSongSyncing(response.body().getModifiedServerObjects(), deletedNotSyncedSongs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<BatchModifyResponse> call, Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void uploadModifiedNoteSheets(List<NoteSheet> modifiedNoteSheets, UploadNoteSheetCallback callback) {
|
||||
List<UploadResponse> uploadResponses = new ArrayList<>();
|
||||
for(NoteSheet noteSheet : modifiedNoteSheets) {
|
||||
RequestBody server_filename = RequestBody.create(MediaType.parse("text/plain"), noteSheet.getServerFileName());
|
||||
|
||||
RequestBody requestFile = RequestBody.create(MediaType.parse("image/**"), new File(noteSheet.getLocalFileName()));
|
||||
MultipartBody.Part image = MultipartBody.Part.createFormData("image", noteSheet.getLocalFileName(), requestFile);
|
||||
|
||||
songSyncAPI.updateNoteSheet(server_filename, image).enqueue(new Callback<UploadResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<UploadResponse> call, Response<UploadResponse> response) {
|
||||
if(response.isSuccessful() && response.body() != null) {
|
||||
uploadResponses.add(response.body());
|
||||
if(uploadResponses.size() == modifiedNoteSheets.size()) {
|
||||
callback.finishUploadNoteSheets(uploadResponses);
|
||||
}
|
||||
} else {
|
||||
Log.d("SongSyncService", "Something went wrong");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<UploadResponse> call, Throwable throwable) {
|
||||
Log.e("SongSyncService", "Upload failed: " + throwable.getMessage(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchRemoteModifiedSongs(LocalDateTime lastClientSync, SongSyncRepository.LoadDataCallback<FetchResponse> callback) {
|
||||
String formattedTime = lastClientSync.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
songSyncAPI.fetchRemoteModifiedSongs(formattedTime).enqueue(new Callback<FetchResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<FetchResponse> call, Response<FetchResponse> response) {
|
||||
if(response.isSuccessful() && response.body() != null) {
|
||||
callback.onResult(response.body());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<FetchResponse> call, Throwable throwable) {
|
||||
Log.d("SongSyncService", "Fetch failed: " + throwable.getMessage(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void getRemotelyModifiedSongData(List<Song> remotelyModifiedSongs, SongSyncRepository.LoadDataCallback<List<SongModel>> callback) {
|
||||
List<SongModel> songModels = new ArrayList<>();
|
||||
for(Song song : remotelyModifiedSongs) {
|
||||
songSyncAPI.fetchRemotelyModifiedSongData(song.getServerID()).enqueue(new Callback<SongModel>() {
|
||||
@Override
|
||||
public void onResponse(Call<SongModel> call, Response<SongModel> response) {
|
||||
if(response.isSuccessful() && response.body() != null) {
|
||||
songModels.add(response.body());
|
||||
if(songModels.size() == remotelyModifiedSongs.size()) {
|
||||
callback.onResult(songModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<SongModel> call, Throwable throwable) {
|
||||
Log.d("SongSyncService", "Fetch failed: " + throwable.getMessage(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void downloadNoteSheet(NoteSheet noteSheet, SongSyncRepository.LoadDataCallback<ResponseBody> callback) {
|
||||
songSyncAPI.downloadNotesheet(noteSheet.getServerFileName()).enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
|
||||
if(response.isSuccessful() && response.body() != null) {
|
||||
callback.onResult(response.body());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable throwable) {
|
||||
Log.d("SongSyncService", "Download failed: " + throwable.getMessage(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void recognizeTitle(String firstNoteSheet, RecognizedSongCallback callback) {
|
||||
RequestBody requestFile = RequestBody.create(MediaType.parse("image/jpeg"), new File(firstNoteSheet));
|
||||
MultipartBody.Part image = MultipartBody.Part.createFormData("image", firstNoteSheet, requestFile);
|
||||
songSyncAPI.recognizeTitle(image).enqueue(new Callback<AIRecognizedSong>() {
|
||||
@Override
|
||||
public void onResponse(Call<AIRecognizedSong> call, Response<AIRecognizedSong> response) {
|
||||
callback.onRecognizedSong(response.body());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<AIRecognizedSong> call, Throwable throwable) {
|
||||
Log.d("SongSyncService", "Recognition failed: " + throwable.getMessage(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public interface SyncDeletedSongsCallback {
|
||||
void finishSongSyncing(List<String> remoteDeletedSongs, List<Integer> localDeletedSongs);
|
||||
}
|
||||
|
||||
public interface UploadNoteSheetCallback {
|
||||
void finishUploadNoteSheets(List<UploadResponse> uploadResponses);
|
||||
}
|
||||
|
||||
public interface RecognizedSongCallback {
|
||||
void onRecognizedSong(AIRecognizedSong aiRecognizedSong);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
public class AIRecognizedSong {
|
||||
private String title;
|
||||
private int year;
|
||||
private String composer;
|
||||
private String description;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
public void setYear(int year) {
|
||||
this.year = year;
|
||||
}
|
||||
|
||||
public String getComposer() {
|
||||
return composer;
|
||||
}
|
||||
|
||||
public void setComposer(String composer) {
|
||||
this.composer = composer;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BatchCreateResponse {
|
||||
private List<CreateResponse> createResponses;
|
||||
|
||||
public List<CreateResponse> getCreateResponses() {
|
||||
return createResponses;
|
||||
}
|
||||
|
||||
public void setCreateResponses(List<CreateResponse> createResponses) {
|
||||
this.createResponses = createResponses;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BatchModifyResponse {
|
||||
private List<String> modifiedServerObjects;
|
||||
|
||||
public List<String> getModifiedServerObjects() {
|
||||
return modifiedServerObjects;
|
||||
}
|
||||
|
||||
public void setModifiedServerObjects(List<String> modifiedServerObjects) {
|
||||
this.modifiedServerObjects = modifiedServerObjects;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
public class CreateResponse {
|
||||
private int localID;
|
||||
private String serverID;
|
||||
|
||||
public int getLocalID() {
|
||||
return localID;
|
||||
}
|
||||
|
||||
public void setLocalID(int localID) {
|
||||
this.localID = localID;
|
||||
}
|
||||
|
||||
public String getServerID() {
|
||||
return serverID;
|
||||
}
|
||||
|
||||
public void setServerID(String serverID) {
|
||||
this.serverID = serverID;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FetchResponse {
|
||||
private List<String> serverIDs;
|
||||
|
||||
public List<String> getServerIDs() {
|
||||
return serverIDs;
|
||||
}
|
||||
|
||||
public void setServerIDs(List<String> serverIDs) {
|
||||
this.serverIDs = serverIDs;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
public class NoteSheetModifyRequest {
|
||||
private String serverFileName;
|
||||
private String clientHash;
|
||||
|
||||
public NoteSheetModifyRequest(String serverFileName, String clientHash) {
|
||||
this.serverFileName = serverFileName;
|
||||
this.clientHash = clientHash;
|
||||
}
|
||||
|
||||
public String getServerFileName() {
|
||||
return serverFileName;
|
||||
}
|
||||
|
||||
public void setServerFileName(String serverFileName) {
|
||||
this.serverFileName = serverFileName;
|
||||
}
|
||||
|
||||
public String getClientHash() {
|
||||
return clientHash;
|
||||
}
|
||||
|
||||
public void setClientHash(String clientHash) {
|
||||
this.clientHash = clientHash;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
public class RemotelyModifiedNoteSheetModel {
|
||||
private String filename;
|
||||
private String server_filename;
|
||||
private String hash;
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public String getServer_filename() {
|
||||
return server_filename;
|
||||
}
|
||||
|
||||
public void setServer_filename(String server_filename) {
|
||||
this.server_filename = server_filename;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public void setHash(String hash) {
|
||||
this.hash = hash;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SongBatchDeleteRequest {
|
||||
private List<String> songs;
|
||||
|
||||
public SongBatchDeleteRequest(List<String> songs) {
|
||||
this.songs = songs;
|
||||
}
|
||||
|
||||
public List<String> getSongs() {
|
||||
return songs;
|
||||
}
|
||||
|
||||
public void setSongs(List<String> songs) {
|
||||
this.songs = songs;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SongCreateBatchRequest {
|
||||
|
||||
private List<SongCreateRequest> songs;
|
||||
|
||||
public SongCreateBatchRequest(List<SongCreateRequest> songs) {
|
||||
this.songs = songs;
|
||||
}
|
||||
|
||||
public List<SongCreateRequest> getSongs() {
|
||||
return songs;
|
||||
}
|
||||
|
||||
public void setSongs(List<SongCreateRequest> songs) {
|
||||
this.songs = songs;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
|
||||
public class SongCreateRequest {
|
||||
private int localID;
|
||||
private String title;
|
||||
private String composer;
|
||||
private String genre;
|
||||
private int year;
|
||||
|
||||
public SongCreateRequest(int localID, String title, String composer, String genre, int year) {
|
||||
this.localID = localID;
|
||||
this.title = title;
|
||||
this.composer = composer;
|
||||
this.genre = genre;
|
||||
this.year = year;
|
||||
}
|
||||
|
||||
public SongCreateRequest(Song song) {
|
||||
this.localID = song.getLocalID();
|
||||
this.title = song.getTitle();
|
||||
this.composer = song.getComposer();
|
||||
this.genre = song.getGenre();
|
||||
this.year = song.getYear();
|
||||
}
|
||||
|
||||
public int getLocalID() {
|
||||
return localID;
|
||||
}
|
||||
|
||||
public void setLocalID(int localID) {
|
||||
this.localID = localID;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getComposer() {
|
||||
return composer;
|
||||
}
|
||||
|
||||
public void setComposer(String composer) {
|
||||
this.composer = composer;
|
||||
}
|
||||
|
||||
public String getGenre() {
|
||||
return genre;
|
||||
}
|
||||
|
||||
public void setGenre(String genre) {
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
public void setYear(int year) {
|
||||
this.year = year;
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SongModel {
|
||||
private String serverID;
|
||||
private String title;
|
||||
private String composer;
|
||||
private String genre;
|
||||
private int year;
|
||||
private List<RemotelyModifiedNoteSheetModel> note_sheets;
|
||||
private boolean deleted;
|
||||
|
||||
public String getServerID() {
|
||||
return serverID;
|
||||
}
|
||||
|
||||
public void setServerID(String serverID) {
|
||||
this.serverID = serverID;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getComposer() {
|
||||
return composer;
|
||||
}
|
||||
|
||||
public void setComposer(String composer) {
|
||||
this.composer = composer;
|
||||
}
|
||||
|
||||
public String getGenre() {
|
||||
return genre;
|
||||
}
|
||||
|
||||
public void setGenre(String genre) {
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
public void setYear(int year) {
|
||||
this.year = year;
|
||||
}
|
||||
|
||||
public List<RemotelyModifiedNoteSheetModel> getNote_sheets() {
|
||||
return note_sheets;
|
||||
}
|
||||
|
||||
public void setNote_sheets(List<RemotelyModifiedNoteSheetModel> note_sheets) {
|
||||
this.note_sheets = note_sheets;
|
||||
}
|
||||
|
||||
public boolean isDeleted() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public void setDeleted(boolean deleted) {
|
||||
this.deleted = deleted;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SongModifyBatchRequest {
|
||||
private List<SongModifyRequest> songs;
|
||||
|
||||
public SongModifyBatchRequest(List<SongModifyRequest> songs) {
|
||||
this.songs = songs;
|
||||
}
|
||||
|
||||
public List<SongModifyRequest> getSongs() {
|
||||
return songs;
|
||||
}
|
||||
|
||||
public void setSongs(List<SongModifyRequest> songs) {
|
||||
this.songs = songs;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SongModifyBatchResponse {
|
||||
|
||||
private List<SongModifyResponse> modifiedServerObjects;
|
||||
|
||||
public List<SongModifyResponse> getModifiedServerObjects() {
|
||||
return modifiedServerObjects;
|
||||
}
|
||||
|
||||
public void setModifiedServerObjects(List<SongModifyResponse> modifiedServerObjects) {
|
||||
this.modifiedServerObjects = modifiedServerObjects;
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SongModifyRequest {
|
||||
|
||||
private String serverID;
|
||||
private String title;
|
||||
private String composer;
|
||||
private String genre;
|
||||
private int year;
|
||||
private List<NoteSheetModifyRequest> noteSheets;
|
||||
|
||||
public SongModifyRequest(String serverID, String title, String composer, String genre, int year) {
|
||||
this.serverID = serverID;
|
||||
this.title = title;
|
||||
this.composer = composer;
|
||||
this.genre = genre;
|
||||
this.year = year;
|
||||
}
|
||||
|
||||
public SongModifyRequest(Song song, List<NoteSheet> noteSheets) {
|
||||
this.serverID = song.getServerID();
|
||||
this.title = song.getTitle();
|
||||
this.composer = song.getComposer();
|
||||
this.genre = song.getGenre();
|
||||
this.year = song.getYear();
|
||||
this.noteSheets = noteSheets.stream().map(noteSheet -> new NoteSheetModifyRequest(noteSheet.getServerFileName(), noteSheet.getHash())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public String getServerID() {
|
||||
return serverID;
|
||||
}
|
||||
|
||||
public void setServerID(String serverID) {
|
||||
this.serverID = serverID;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getComposer() {
|
||||
return composer;
|
||||
}
|
||||
|
||||
public void setComposer(String composer) {
|
||||
this.composer = composer;
|
||||
}
|
||||
|
||||
public String getGenre() {
|
||||
return genre;
|
||||
}
|
||||
|
||||
public void setGenre(String genre) {
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
public void setYear(int year) {
|
||||
this.year = year;
|
||||
}
|
||||
|
||||
public List<NoteSheetModifyRequest> getNoteSheets() {
|
||||
return noteSheets;
|
||||
}
|
||||
|
||||
public void setNoteSheets(List<NoteSheetModifyRequest> noteSheets) {
|
||||
this.noteSheets = noteSheets;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SongModifyResponse {
|
||||
|
||||
private String serverID;
|
||||
private List<String> outdated_note_sheets;
|
||||
|
||||
public List<String> getOutdated_note_sheets() {
|
||||
return outdated_note_sheets;
|
||||
}
|
||||
|
||||
public void setOutdated_note_sheets(List<String> outdated_note_sheets) {
|
||||
this.outdated_note_sheets = outdated_note_sheets;
|
||||
}
|
||||
|
||||
public String getServerID() {
|
||||
return serverID;
|
||||
}
|
||||
|
||||
public void setServerID(String serverID) {
|
||||
this.serverID = serverID;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.stormtales.notevault.network.sync.models;
|
||||
|
||||
public class UploadResponse {
|
||||
private String serverID;
|
||||
private String localFile;
|
||||
private String serverFile;
|
||||
|
||||
public String getServerID() {
|
||||
return serverID;
|
||||
}
|
||||
|
||||
public void setServerID(String serverID) {
|
||||
this.serverID = serverID;
|
||||
}
|
||||
|
||||
public String getLocalFile() {
|
||||
return localFile;
|
||||
}
|
||||
|
||||
public void setLocalFile(String localFile) {
|
||||
this.localFile = localFile;
|
||||
}
|
||||
|
||||
public String getServerFile() {
|
||||
return serverFile;
|
||||
}
|
||||
|
||||
public void setServerFile(String serverFile) {
|
||||
this.serverFile = serverFile;
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package com.stormtales.notevault.ui.gallery;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.stormtales.notevault.data.repositories.SongRepository;
|
||||
import com.stormtales.notevault.databinding.FragmentGalleryBinding;
|
||||
import com.stormtales.notevault.network.sync.SongSyncModule;
|
||||
import com.stormtales.notevault.network.sync.SongSyncService;
|
||||
|
||||
|
||||
public class GalleryFragment extends Fragment {
|
||||
|
||||
GalleryViewModel galleryViewModel;
|
||||
private FragmentGalleryBinding binding;
|
||||
private ProgressBar progress_sync_created_songs;
|
||||
private Button sync_created_songs_btn;
|
||||
private ProgressBar progress_sync_modified_songs;
|
||||
private Button sync_modified_songs_btn;
|
||||
private ProgressBar progress_sync_deleted_songs;
|
||||
private Button sync_deleted_songs_btn;
|
||||
|
||||
private ProgressBar progress_fetching_songs;
|
||||
private Button sync_fetching_songs_btn;
|
||||
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
galleryViewModel = new ViewModelProvider(this).get(GalleryViewModel.class);
|
||||
|
||||
binding = FragmentGalleryBinding.inflate(inflater, container, false);
|
||||
View root = binding.getRoot();
|
||||
|
||||
progress_sync_created_songs = binding.progressSyncCreatedSongs;
|
||||
sync_created_songs_btn = binding.syncCreatedBtn;
|
||||
sync_created_songs_btn.setOnClickListener(v -> onSyncCreatedSongs());
|
||||
|
||||
progress_sync_modified_songs = binding.progressSyncModifiedSongs;
|
||||
sync_modified_songs_btn = binding.syncModifiedBtn;
|
||||
sync_modified_songs_btn.setOnClickListener(v -> onSyncModifiedSogs());
|
||||
|
||||
progress_sync_deleted_songs = binding.progressSyncDeletedSongs;
|
||||
sync_deleted_songs_btn = binding.syncDeletedBtn;
|
||||
sync_deleted_songs_btn.setOnClickListener(v -> onSyncDeletedSongs());
|
||||
|
||||
progress_fetching_songs = binding.progressFetchSongs;
|
||||
sync_fetching_songs_btn = binding.fetchSongsBtn;
|
||||
sync_fetching_songs_btn.setOnClickListener(v -> onFetchRemoteModifiedSongs());
|
||||
|
||||
galleryViewModel.setSongSyncModule(new SongSyncModule(getContext(), galleryViewModel));
|
||||
|
||||
galleryViewModel.getIsCreatedSongSyncing().observe(getViewLifecycleOwner(), isCreatedSyncinc -> {
|
||||
if(isCreatedSyncinc) {
|
||||
progress_sync_created_songs.setIndeterminate(true);
|
||||
sync_created_songs_btn.setEnabled(false);
|
||||
} else {
|
||||
progress_sync_created_songs.setIndeterminate(false);
|
||||
sync_created_songs_btn.setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
galleryViewModel.getIsModifiedSongSyncing().observe(getViewLifecycleOwner(), isModifiedSyncinc -> {
|
||||
if(isModifiedSyncinc) {
|
||||
progress_sync_modified_songs.setIndeterminate(true);
|
||||
sync_modified_songs_btn.setEnabled(false);
|
||||
} else {
|
||||
progress_sync_modified_songs.setIndeterminate(false);
|
||||
sync_modified_songs_btn.setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
galleryViewModel.getIsDeletedSongSyncing().observe(getViewLifecycleOwner(), isDeletedSyncinc -> {
|
||||
if(isDeletedSyncinc) {
|
||||
progress_sync_deleted_songs.setIndeterminate(true);
|
||||
sync_deleted_songs_btn.setEnabled(false);
|
||||
} else {
|
||||
progress_sync_deleted_songs.setIndeterminate(false);
|
||||
sync_deleted_songs_btn.setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
galleryViewModel.getIsFetchingActive().observe(getViewLifecycleOwner(), isFetchingActiveSyncinc -> {
|
||||
if(isFetchingActiveSyncinc) {
|
||||
progress_fetching_songs.setIndeterminate(true);
|
||||
sync_fetching_songs_btn.setEnabled(false);
|
||||
} else {
|
||||
progress_fetching_songs.setIndeterminate(false);
|
||||
sync_fetching_songs_btn.setEnabled(true);
|
||||
}
|
||||
});
|
||||
return root;
|
||||
}
|
||||
|
||||
private void onFetchRemoteModifiedSongs() {
|
||||
galleryViewModel.startFetchingActive();
|
||||
}
|
||||
|
||||
private void onSyncDeletedSongs() {
|
||||
galleryViewModel.startDeletedSongSyncing();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void onSyncCreatedSongs() {
|
||||
galleryViewModel.startCreateSongSyncing();
|
||||
}
|
||||
|
||||
|
||||
private void onSyncModifiedSogs() {
|
||||
galleryViewModel.startModifiedSongSyncinc();
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package com.stormtales.notevault.ui.gallery;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import com.stormtales.notevault.data.repositories.SongRepository;
|
||||
import com.stormtales.notevault.network.sync.SongSyncModule;
|
||||
|
||||
public class GalleryViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Boolean> isCreatedSongSyncing;
|
||||
private final MutableLiveData<Boolean> isModifiedSongSyncing;
|
||||
private final MutableLiveData<Boolean> isDeletedSongSyncing;
|
||||
private final MutableLiveData<Boolean> isFetchingActive;
|
||||
private SongSyncModule songSyncModule;
|
||||
|
||||
public GalleryViewModel() {
|
||||
|
||||
isCreatedSongSyncing = new MutableLiveData<>();
|
||||
isCreatedSongSyncing.setValue(false);
|
||||
|
||||
isModifiedSongSyncing = new MutableLiveData<>();
|
||||
isModifiedSongSyncing.setValue(false);
|
||||
|
||||
isDeletedSongSyncing = new MutableLiveData<>();
|
||||
isDeletedSongSyncing.setValue(false);
|
||||
|
||||
isFetchingActive = new MutableLiveData<>();
|
||||
isFetchingActive.setValue(false);
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getIsCreatedSongSyncing() {
|
||||
return isCreatedSongSyncing;
|
||||
}
|
||||
|
||||
public void startCreateSongSyncing() {
|
||||
if(this.songSyncModule == null) throw new RuntimeException("SongSyncModule is not initialized");
|
||||
this.isCreatedSongSyncing.setValue(true);
|
||||
|
||||
this.songSyncModule.syncCreatedSongs();
|
||||
}
|
||||
|
||||
public void finishCreateSongSyncing() {
|
||||
this.isCreatedSongSyncing.setValue(false);
|
||||
}
|
||||
|
||||
public void setSongSyncModule(SongSyncModule songSyncModule) {
|
||||
this.songSyncModule = songSyncModule;
|
||||
}
|
||||
|
||||
public void startModifiedSongSyncinc() {
|
||||
if(this.songSyncModule == null) throw new RuntimeException("SongSyncModule is not initialized");
|
||||
this.isModifiedSongSyncing.setValue(true);
|
||||
this.songSyncModule.syncModifiedSongs();
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getIsModifiedSongSyncing() {
|
||||
return isModifiedSongSyncing;
|
||||
}
|
||||
|
||||
public void finishModifiedSongSyncinc() {
|
||||
this.isModifiedSongSyncing.setValue(false);
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getIsDeletedSongSyncing() {
|
||||
return isDeletedSongSyncing;
|
||||
}
|
||||
|
||||
public void startDeletedSongSyncing() {
|
||||
if(this.songSyncModule == null) throw new RuntimeException("SongSyncModule is not initialized");
|
||||
this.isDeletedSongSyncing.setValue(true);
|
||||
this.songSyncModule.syncDeletedSongs();
|
||||
}
|
||||
|
||||
public void finishDeleteSongSyncinc() {
|
||||
this.isDeletedSongSyncing.setValue(false);
|
||||
}
|
||||
|
||||
public void startFetchingActive() {
|
||||
if(this.songSyncModule == null) throw new RuntimeException("SongSyncModule is not initialized");
|
||||
this.isFetchingActive.setValue(true);
|
||||
this.songSyncModule.fetchRemoteModifiedSongs();
|
||||
}
|
||||
|
||||
public void finishFetching() {
|
||||
this.isFetchingActive.setValue(false);
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getIsFetchingActive() {
|
||||
return isFetchingActive;
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package com.stormtales.notevault.ui.home;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.stormtales.notevault.R;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.repositories.SongRepository;
|
||||
import com.stormtales.notevault.databinding.FragmentHomeBinding;
|
||||
import com.stormtales.notevault.ui.sheetdisplay.NoteSheetDisplayActivity;
|
||||
import com.stormtales.notevault.ui.songeditor.SongEditorDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class HomeFragment extends Fragment {
|
||||
|
||||
private FragmentHomeBinding binding;
|
||||
private RecyclerView recyclerView;
|
||||
private SongAdapter songAdapter;
|
||||
private HomeViewModel homeViewModel;
|
||||
private static final int PICK_FILE_REQUEST_CODE = 1;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
|
||||
homeViewModel.setSongRepository(new SongRepository(this.getContext()));
|
||||
|
||||
binding = FragmentHomeBinding.inflate(inflater, container, false);
|
||||
View root = binding.getRoot();
|
||||
recyclerView = root.findViewById(R.id.recycler_view_songs);
|
||||
|
||||
ActivityResultLauncher<Intent> launcher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
activityResult -> {
|
||||
getActivity();
|
||||
if(activityResult.getResultCode() == Activity.RESULT_OK && activityResult.getData() != null) {
|
||||
if(activityResult.getData().getClipData() != null) {
|
||||
int fileNumber = activityResult.getData().getClipData().getItemCount();
|
||||
Uri[] files = new Uri[fileNumber];
|
||||
for(int i = 0; i < fileNumber; i++) {
|
||||
files[i] = activityResult.getData().getClipData().getItemAt(i).getUri();
|
||||
Toast.makeText(requireContext(), "Uri: " + files[i].toString(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
handleSelectedNoteSheets(files);
|
||||
} else {
|
||||
Uri uri = activityResult.getData().getData();
|
||||
Toast.makeText(requireContext(), "Uri: " + uri.toString(), Toast.LENGTH_SHORT).show();
|
||||
handleSelectedNoteSheets(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
FloatingActionButton importBtn = root.findViewById(R.id.addSongBtn);
|
||||
importBtn.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.setType("*/*"); // Alle Dateitypen
|
||||
|
||||
String[] mimeTypes = {"application/pdf", "image/png", "image/jpeg"};
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
|
||||
|
||||
launcher.launch(intent);
|
||||
});
|
||||
|
||||
// RecyclerView einrichten
|
||||
songAdapter = new SongAdapter(new ArrayList<>(), this::onEditSong, this::onDeleteSong, this::onOpenSong);
|
||||
recyclerView.setAdapter(songAdapter);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(), LinearLayoutManager.VERTICAL);
|
||||
recyclerView.addItemDecoration(dividerItemDecoration);
|
||||
|
||||
// Beobachte Änderungen in der Song-Liste
|
||||
homeViewModel.getAllSongsLive().observe(getViewLifecycleOwner(), songs -> {
|
||||
songAdapter.updateData(songs);
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void handleSelectedNoteSheets(Uri... files) {
|
||||
SongEditorDialog songEditorDialog = new SongEditorDialog();
|
||||
songEditorDialog.setNoteSheetFiles(files);
|
||||
songEditorDialog.setHomeViewModel(homeViewModel);
|
||||
songEditorDialog.show(getParentFragmentManager(), "songEditorDialog");
|
||||
}
|
||||
|
||||
public void onEditSong(Song song) {
|
||||
SongEditorDialog songEditorDialog = new SongEditorDialog();
|
||||
songEditorDialog.setEditedSong(song);
|
||||
songEditorDialog.setHomeViewModel(homeViewModel);
|
||||
songEditorDialog.show(getParentFragmentManager(), "songEditorDialog");
|
||||
}
|
||||
|
||||
public void onDeleteSong(Song song) {
|
||||
this.homeViewModel.deleteSong(song);
|
||||
}
|
||||
|
||||
public void onOpenSong(Song song) {
|
||||
homeViewModel.getSongRepository().getNoteSheetFilesBySongID(song.getLocalID(), result -> {
|
||||
String[] noteSheetFiles = new String[result.size()];
|
||||
for(int i = 0; i < result.size(); i++) {
|
||||
noteSheetFiles[i] = result.get(i);
|
||||
}
|
||||
Intent intent = new Intent(getContext(), NoteSheetDisplayActivity.class);
|
||||
intent.putExtra("imageUris", noteSheetFiles);
|
||||
getContext().startActivity(intent);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package com.stormtales.notevault.ui.home;
|
||||
|
||||
import android.util.Log;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.repositories.SongRepository;
|
||||
import com.stormtales.notevault.data.sync.SyncStatus;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class HomeViewModel extends ViewModel {
|
||||
|
||||
private MutableLiveData<List<Song>> allSongs;
|
||||
private SongRepository songRepository;
|
||||
|
||||
public HomeViewModel() {
|
||||
List<Song> currentSongs = new ArrayList<>();
|
||||
this.allSongs = new MutableLiveData<>(currentSongs);
|
||||
/*this.allSongs.setValue(songRepository.getAllSongs());*/
|
||||
}
|
||||
|
||||
public void addSong(Song song, List<NoteSheet> noteSheetList) {
|
||||
songRepository.insert(song, internalSongID -> {
|
||||
for(NoteSheet noteSheet : noteSheetList) {
|
||||
noteSheet.setSongID(Math.toIntExact(internalSongID));
|
||||
}
|
||||
songRepository.insertNoteSheets(noteSheetList);
|
||||
});
|
||||
|
||||
|
||||
List<Song> currentSongs = allSongs.getValue();
|
||||
if(currentSongs == null) {
|
||||
currentSongs = new ArrayList<>();
|
||||
}
|
||||
|
||||
// Neue Liste erstellen und den Song hinzufügen
|
||||
List<Song> updatedSongs = new ArrayList<>(currentSongs);
|
||||
updatedSongs.add(song);
|
||||
// Neue Liste in MutableLiveData setzen
|
||||
allSongs.setValue(updatedSongs);
|
||||
Log.d("HomeViewModel", "Song added. Total songs: " + updatedSongs.size());
|
||||
}
|
||||
|
||||
public LiveData<List<Song>> getAllSongsLive() {
|
||||
return allSongs;
|
||||
}
|
||||
|
||||
public void setSongRepository(SongRepository songRepository) {
|
||||
this.songRepository = songRepository;
|
||||
loadAllSongs();
|
||||
}
|
||||
|
||||
public void loadAllSongs() {
|
||||
songRepository.getAllSongs(songs->allSongs.setValue(songs));
|
||||
}
|
||||
|
||||
public void deleteSong(Song song) {
|
||||
songRepository.deleteSong(song);
|
||||
|
||||
List<Song> currentSongs = allSongs.getValue();
|
||||
if(currentSongs != null) {
|
||||
currentSongs.remove(song);
|
||||
allSongs.setValue(currentSongs);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateSong(Song editedSong) {
|
||||
List<Song> currentSongs = allSongs.getValue();
|
||||
if(currentSongs != null) {
|
||||
for(int i = 0; i < currentSongs.size(); i++) {
|
||||
if(currentSongs.get(i).getLocalID() == editedSong.getLocalID()) {
|
||||
currentSongs.set(i, editedSong);
|
||||
break;
|
||||
}
|
||||
}
|
||||
songRepository.updateSong(editedSong);
|
||||
allSongs.setValue(currentSongs);
|
||||
}
|
||||
}
|
||||
|
||||
public SongRepository getSongRepository() {
|
||||
return songRepository;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package com.stormtales.notevault.ui.home;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.stormtales.notevault.R;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SongAdapter extends RecyclerView.Adapter<SongAdapter.SongViewHolder> {
|
||||
|
||||
private List<Song> songList;
|
||||
private OnEditSongListener onEditSongListener;
|
||||
private OnDeleteSongListener onDeleteSongListener;
|
||||
private OnOpenSongListener onOpenSongListener;
|
||||
|
||||
public SongAdapter(List<Song> songList, OnEditSongListener onEditSongListener, OnDeleteSongListener onDeleteSongListener, OnOpenSongListener onOpenSongListener) {
|
||||
this.songList = songList;
|
||||
this.onEditSongListener = onEditSongListener;
|
||||
this.onDeleteSongListener = onDeleteSongListener;
|
||||
this.onOpenSongListener = onOpenSongListener;
|
||||
}
|
||||
|
||||
public void updateData(List<Song> songs) {
|
||||
if (songs == null) {
|
||||
this.songList = new ArrayList<>();
|
||||
} else {
|
||||
this.songList = new ArrayList<>(songs);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
Log.d("SongAdapter", "Data updated: " + this.songList.size());
|
||||
}
|
||||
|
||||
public class SongViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView textYear, textComposition, textTitle;
|
||||
ImageButton editButton, deleteButton;
|
||||
|
||||
public SongViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
textYear = itemView.findViewById(R.id.text_year);
|
||||
textComposition = itemView.findViewById(R.id.text_composition);
|
||||
textTitle = itemView.findViewById(R.id.text_title);
|
||||
editButton = itemView.findViewById(R.id.edit_song_button);
|
||||
deleteButton = itemView.findViewById(R.id.delete_song_button);
|
||||
|
||||
textTitle.setOnClickListener(v -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
Song song = songList.get(position);
|
||||
onOpenSongListener.onOpenSong(song);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public @NotNull SongViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int i) {
|
||||
View itemView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_song, parent, false);
|
||||
return new SongViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull @NotNull SongAdapter.SongViewHolder holder, int position) {
|
||||
Song song = songList.get(position);
|
||||
holder.textYear.setText(String.valueOf(song.getYear()));
|
||||
holder.textComposition.setText(song.getComposer());
|
||||
holder.textTitle.setText(song.getTitle());
|
||||
|
||||
holder.editButton.setOnClickListener(v -> onEditSongListener.onEditSong(song));
|
||||
holder.deleteButton.setOnClickListener(v -> onDeleteSongListener.onDeleteSong(song));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return songList.size();
|
||||
}
|
||||
|
||||
public interface OnEditSongListener {
|
||||
void onEditSong(Song song);
|
||||
}
|
||||
|
||||
public interface OnDeleteSongListener {
|
||||
void onDeleteSong(Song song);
|
||||
}
|
||||
|
||||
public interface OnOpenSongListener {
|
||||
void onOpenSong(Song song);
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
package com.stormtales.notevault.ui.login;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.stormtales.notevault.R;
|
||||
import com.stormtales.notevault.network.APICallback;
|
||||
import com.stormtales.notevault.network.auth.AuthService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class LoginDialog extends DialogFragment {
|
||||
|
||||
private LoginViewModel loginViewModel;
|
||||
private boolean isLoginMode = true;
|
||||
private Dialog dialog;
|
||||
|
||||
private Button loginButton;
|
||||
private ProgressBar loadingProgressBar;
|
||||
private EditText editTextUsername;
|
||||
private EditText editTextPassword;
|
||||
private EditText editTextEmail;
|
||||
private TextView textViewTitle;
|
||||
private TextView textViewSwitch;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
}
|
||||
|
||||
public LoginDialog(LoginViewModel loginViewModel) {
|
||||
this.loginViewModel = loginViewModel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
LayoutInflater inflater = requireActivity().getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.fragment_login_dialog, null);
|
||||
|
||||
textViewTitle = view.findViewById(R.id.textViewTitle);
|
||||
editTextUsername = view.findViewById(R.id.editTextUsername);
|
||||
editTextEmail = view.findViewById(R.id.editTextEmail);
|
||||
editTextPassword = view.findViewById(R.id.editTextPassword);
|
||||
loginButton = view.findViewById(R.id.buttonAction);
|
||||
textViewSwitch = view.findViewById(R.id.textViewSwitch);
|
||||
loadingProgressBar = view.findViewById(R.id.loading);
|
||||
// Handle action button click
|
||||
|
||||
|
||||
// Toggle between Login and Registration
|
||||
textViewSwitch.setOnClickListener(v -> {
|
||||
isLoginMode = !isLoginMode;
|
||||
textViewTitle.setText(isLoginMode ? "Login" : "Register");
|
||||
editTextUsername.setVisibility(isLoginMode ? View.GONE : View.VISIBLE);
|
||||
loginButton.setText(isLoginMode ? "Login" : "Register");
|
||||
textViewSwitch.setText(isLoginMode ? "Don't have an account? Register" : "Already have an account? Login");
|
||||
});
|
||||
|
||||
builder.setView(view);
|
||||
dialog = builder.create();
|
||||
|
||||
|
||||
|
||||
TextWatcher afterTextChangedListener = new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
loginViewModel.updateLoginData(editTextUsername.getText().toString(), editTextPassword.getText().toString());
|
||||
}
|
||||
};
|
||||
|
||||
editTextUsername.addTextChangedListener(afterTextChangedListener);
|
||||
editTextPassword.addTextChangedListener(afterTextChangedListener);
|
||||
editTextPassword.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if(actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
loginViewModel.performLogin(editTextUsername.getText().toString(), editTextPassword.getText().toString(), LoginDialog.this::onSuccessFullLogin);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
loginButton.setOnClickListener(v -> {
|
||||
loadingProgressBar.setVisibility(View.VISIBLE);
|
||||
String email = editTextEmail.getText().toString();
|
||||
String password = editTextPassword.getText().toString();
|
||||
if (isLoginMode) {
|
||||
// Handle login logic
|
||||
this.loginViewModel.performLogin(email, password, this::onSuccessFullLogin);
|
||||
} else {
|
||||
// Handle registration logic
|
||||
String username = editTextUsername.getText().toString();
|
||||
this.loginViewModel.performRegistration(email, password, username, this::onSuccessFullRegistration);
|
||||
}
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void onSuccessFullRegistration() {
|
||||
Toast.makeText(getContext(), "Successfully registered", Toast.LENGTH_LONG).show();
|
||||
this.isLoginMode = true;
|
||||
textViewTitle.setText(isLoginMode ? "Login" : "Register");
|
||||
editTextUsername.setVisibility(isLoginMode ? View.GONE : View.VISIBLE);
|
||||
loginButton.setText(isLoginMode ? "Login" : "Register");
|
||||
textViewSwitch.setText(isLoginMode ? "Don't have an account? Register" : "Already have an account? Login");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull @NotNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
loginViewModel.getLoginFormState().observe(getViewLifecycleOwner(), new Observer<LoginFormState>() {
|
||||
@Override
|
||||
public void onChanged(LoginFormState loginFormState) {
|
||||
if(loginFormState == null) return;
|
||||
loginButton.setEnabled(loginFormState.isDataValid());
|
||||
if(loginFormState.getUsernameError() != null) {
|
||||
editTextUsername.setError(getString(loginFormState.getUsernameError()));
|
||||
}
|
||||
if(loginFormState.getPasswordError() != null) {
|
||||
editTextPassword.setError(getString(loginFormState.getPasswordError()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onSuccessFullLogin() {
|
||||
this.dialog.dismiss();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.stormtales.notevault.ui.login;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class LoginFormState {
|
||||
@Nullable
|
||||
private Integer usernameError;
|
||||
@Nullable
|
||||
private Integer passwordError;
|
||||
private boolean isDataValid;
|
||||
|
||||
LoginFormState(@Nullable Integer usernameError, @Nullable Integer passwordError) {
|
||||
this.usernameError = usernameError;
|
||||
this.passwordError = passwordError;
|
||||
this.isDataValid = false;
|
||||
}
|
||||
|
||||
LoginFormState(boolean isDataValid) {
|
||||
this.usernameError = null;
|
||||
this.passwordError = null;
|
||||
this.isDataValid = isDataValid;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Integer getUsernameError() {
|
||||
return usernameError;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Integer getPasswordError() {
|
||||
return passwordError;
|
||||
}
|
||||
|
||||
boolean isDataValid() {
|
||||
return isDataValid;
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package com.stormtales.notevault.ui.login;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Patterns;
|
||||
import android.widget.Toast;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import com.stormtales.notevault.R;
|
||||
import com.stormtales.notevault.network.APICallback;
|
||||
import com.stormtales.notevault.network.auth.AuthService;
|
||||
import com.stormtales.notevault.network.auth.LoginResponse;
|
||||
|
||||
public class LoginViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<String> username;
|
||||
private final MutableLiveData<Boolean> isLoggedIn;
|
||||
private MutableLiveData<LoginFormState> loginFormState = new MutableLiveData<>();
|
||||
private AuthService authService;
|
||||
|
||||
public LoginViewModel() {
|
||||
username = new MutableLiveData<>("Not Logged In");
|
||||
isLoggedIn = new MutableLiveData<>(false);
|
||||
}
|
||||
|
||||
public void performLogin(String email, String password, SuccessFullLoginCallback successFullLoginCallback) {
|
||||
if(authService != null) {
|
||||
authService.performLogin(email, password, new LoginCallBackImpl(successFullLoginCallback));
|
||||
} else {
|
||||
throw new RuntimeException("AuthService is not provided to LoginViewModel");
|
||||
}
|
||||
}
|
||||
|
||||
public void performRegistration(String email, String password, String username, SuccessFullLoginCallback successFullLoginCallback) {
|
||||
if(authService != null) {
|
||||
authService.performRegistration(email, username, password, new APICallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
successFullLoginCallback.onSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.d("LoginService", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void setAuthService(AuthService authService) {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getIsLoggedIn() {
|
||||
return isLoggedIn;
|
||||
}
|
||||
|
||||
public MutableLiveData<String> getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void updateLoginData(String username, String password) {
|
||||
if(!isUserNameValid(username)) {
|
||||
loginFormState.setValue(new LoginFormState(R.string.invalid_username, null));
|
||||
} else if(!isPasswordValid(password)) {
|
||||
loginFormState.setValue(new LoginFormState(null, R.string.invalid_password));
|
||||
} else {
|
||||
loginFormState.setValue(new LoginFormState(true));
|
||||
}
|
||||
}
|
||||
|
||||
public class LoginCallBackImpl implements AuthService.LoginCallback {
|
||||
|
||||
private final SuccessFullLoginCallback successFullLoginCallback;
|
||||
|
||||
public LoginCallBackImpl(SuccessFullLoginCallback successFullLoginCallback) {
|
||||
this.successFullLoginCallback = successFullLoginCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(LoginResponse loginResponse) {
|
||||
username.setValue(loginResponse.getUsername());
|
||||
isLoggedIn.setValue(true);
|
||||
successFullLoginCallback.onSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.d("LoginService", error);
|
||||
}
|
||||
}
|
||||
|
||||
public interface SuccessFullLoginCallback {
|
||||
void onSuccess();
|
||||
}
|
||||
|
||||
public MutableLiveData<LoginFormState> getLoginFormState() {
|
||||
return loginFormState;
|
||||
}
|
||||
|
||||
// A placeholder username validation check
|
||||
private boolean isUserNameValid(String username) {
|
||||
if (username == null) {
|
||||
return false;
|
||||
}
|
||||
if (username.contains("@")) {
|
||||
return Patterns.EMAIL_ADDRESS.matcher(username).matches();
|
||||
} else {
|
||||
return !username.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// A placeholder password validation check
|
||||
private boolean isPasswordValid(String password) {
|
||||
return password != null && password.trim().length() > 5;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package com.stormtales.notevault.ui.sheetdisplay;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.github.chrisbanes.photoview.PhotoView;
|
||||
import com.stormtales.notevault.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ImagePagerAdapter extends RecyclerView.Adapter<ImagePagerAdapter.ImageViewHolder> {
|
||||
private final List<String> imageUris;
|
||||
private final Context context;
|
||||
|
||||
public ImagePagerAdapter(Context context, List<String> imageUris) {
|
||||
this.context = context;
|
||||
this.imageUris = imageUris;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.item_image, parent, false);
|
||||
return new ImageViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ImageViewHolder holder, int position) {
|
||||
Uri uri = Uri.parse(imageUris.get(position));
|
||||
holder.photoView.setImageURI(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return imageUris.size();
|
||||
}
|
||||
|
||||
static class ImageViewHolder extends RecyclerView.ViewHolder {
|
||||
PhotoView photoView;
|
||||
|
||||
ImageViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
photoView = itemView.findViewById(R.id.photoView);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.stormtales.notevault.ui.sheetdisplay;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
import com.stormtales.notevault.R;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class NoteSheetDisplayActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_note_sheet_viewer); // Erstelle eine Layout-Datei
|
||||
|
||||
ViewPager2 imageView = findViewById(R.id.viewPager); // Angenommen, du hast ein ImageView
|
||||
TabLayout tabLayout = findViewById(R.id.tabLayout);
|
||||
|
||||
|
||||
|
||||
// Die URI aus dem Intent erhalten
|
||||
Intent intent = getIntent();
|
||||
String[] imageUris = intent.getStringArrayExtra("imageUris");
|
||||
if (imageUris != null) {
|
||||
List<String> uriList = Arrays.asList(imageUris);
|
||||
ImagePagerAdapter adapter = new ImagePagerAdapter(this, uriList);
|
||||
imageView.setAdapter(adapter); // Setze die URI in das ImageView
|
||||
|
||||
new TabLayoutMediator(tabLayout, imageView,
|
||||
(tab, position) -> tab.setText("Sheet " + (position + 1)) // Hier kannst du auch andere Labels verwenden
|
||||
).attach();
|
||||
} else {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.stormtales.notevault.ui.slideshow;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.stormtales.notevault.databinding.FragmentSlideshowBinding;
|
||||
|
||||
public class SlideshowFragment extends Fragment {
|
||||
|
||||
private FragmentSlideshowBinding binding;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
SlideshowViewModel slideshowViewModel =
|
||||
new ViewModelProvider(this).get(SlideshowViewModel.class);
|
||||
|
||||
binding = FragmentSlideshowBinding.inflate(inflater, container, false);
|
||||
View root = binding.getRoot();
|
||||
|
||||
final TextView textView = binding.textSlideshow;
|
||||
slideshowViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.stormtales.notevault.ui.slideshow;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
public class SlideshowViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<String> mText;
|
||||
|
||||
public SlideshowViewModel() {
|
||||
mText = new MutableLiveData<>();
|
||||
mText.setValue("This is slideshow fragment");
|
||||
}
|
||||
|
||||
public LiveData<String> getText() {
|
||||
return mText;
|
||||
}
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
package com.stormtales.notevault.ui.songeditor;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Layout;
|
||||
import android.util.Log;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.stormtales.notevault.R;
|
||||
import com.stormtales.notevault.data.entities.NoteSheet;
|
||||
import com.stormtales.notevault.data.entities.Song;
|
||||
import com.stormtales.notevault.data.sync.SyncStatus;
|
||||
import com.stormtales.notevault.network.sync.SongSyncService;
|
||||
import com.stormtales.notevault.network.sync.models.AIRecognizedSong;
|
||||
import com.stormtales.notevault.ui.home.HomeViewModel;
|
||||
import com.stormtales.notevault.utils.NoteSheetsUtil;
|
||||
import com.stormtales.notevault.utils.Tupel;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class SongEditorDialog extends DialogFragment {
|
||||
|
||||
Dialog dialog;
|
||||
private Uri[] noteSheetFiles;
|
||||
private HomeViewModel homeViewModel;
|
||||
|
||||
private Song editedSong;
|
||||
private SongSyncService songSyncService;
|
||||
|
||||
private View progressbar;
|
||||
public SongEditorDialog() {
|
||||
// Required empty public constructor
|
||||
this.songSyncService = new SongSyncService(this.getContext());
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public @NotNull Dialog onCreateDialog(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
View dialogView = inflater.inflate(R.layout.fragment_song_editor_dialog, null);
|
||||
|
||||
|
||||
dialogView.findViewById(R.id.btnCancel).setOnClickListener(v-> onCancel());
|
||||
dialogView.findViewById(R.id.btnSave).setOnClickListener(v -> onSave());
|
||||
|
||||
if(this.noteSheetFiles == null || noteSheetFiles.length == 0) {
|
||||
dialogView.findViewById(R.id.btnAutoDetect).setEnabled(false);
|
||||
} else {
|
||||
progressbar = dialogView.findViewById(R.id.spinnerAutoDetect);
|
||||
|
||||
}
|
||||
|
||||
dialogView.findViewById(R.id.btnAutoDetect).setOnClickListener(v -> {
|
||||
progressbar.setVisibility(ViewGroup.VISIBLE);
|
||||
extractTitleFromFirstPage();
|
||||
});
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setCancelable(false);
|
||||
builder.setView(dialogView);
|
||||
|
||||
dialog = builder.create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private File createTemporaryFileForRecognition(Uri noteSheetUri) {
|
||||
try {
|
||||
// Hole den InputStream des Bildes
|
||||
InputStream inputStream = getContext().getContentResolver().openInputStream(noteSheetUri);
|
||||
if (inputStream != null) {
|
||||
// Erstelle eine temporäre Datei
|
||||
File tempFile = File.createTempFile("temp_note_sheet", ".jpg", getContext().getCacheDir());
|
||||
|
||||
// Inhalt des InputStreams in die temporäre Datei schreiben
|
||||
try (OutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
// Schließe den InputStream
|
||||
inputStream.close();
|
||||
|
||||
return tempFile;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("SongEditorDialog", "Fehler beim Erstellen der temporären Datei: " + e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
private void extractTitleFromFirstPage() {
|
||||
if(noteSheetFiles != null && noteSheetFiles.length > 0) {
|
||||
Uri firstPageUri = noteSheetFiles[0];
|
||||
|
||||
// Temporäre Datei erstellen
|
||||
File tempFile = createTemporaryFileForRecognition(firstPageUri);
|
||||
if(tempFile != null) {
|
||||
songSyncService.recognizeTitle(tempFile.getAbsolutePath(), new SongSyncService.RecognizedSongCallback() {
|
||||
@Override
|
||||
public void onRecognizedSong(AIRecognizedSong aiRecognizedSong) {
|
||||
progressbar.setVisibility(ViewGroup.GONE);
|
||||
((EditText) dialog.findViewById(R.id.etTitle)).setText(aiRecognizedSong.getTitle());
|
||||
((EditText) dialog.findViewById(R.id.etComposer)).setText(aiRecognizedSong.getComposer());
|
||||
((EditText) dialog.findViewById(R.id.etYear)).setText(String.valueOf(aiRecognizedSong.getYear()));
|
||||
((EditText) dialog.findViewById(R.id.etGenre)).setText(aiRecognizedSong.getDescription());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if(editedSong != null) {
|
||||
((EditText) dialog.findViewById(R.id.etTitle)).setText(editedSong.getTitle());
|
||||
((EditText) dialog.findViewById(R.id.etComposer)).setText(editedSong.getComposer());
|
||||
((EditText) dialog.findViewById(R.id.etGenre)).setText(editedSong.getGenre());
|
||||
((EditText) dialog.findViewById(R.id.etYear)).setText(String.valueOf(editedSong.getYear()));
|
||||
}
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
private void onSave() {
|
||||
String title = ((EditText) dialog.findViewById(R.id.etTitle)).getText().toString();
|
||||
String composer = ((EditText) dialog.findViewById(R.id.etComposer)).getText().toString();
|
||||
|
||||
String year_string = ((EditText) dialog.findViewById(R.id.etYear)).getText().toString();
|
||||
int releaseYear = year_string.isBlank()? 0 : Integer.parseInt(year_string);
|
||||
String genre = ((EditText) dialog.findViewById(R.id.etGenre)).getText().toString();
|
||||
|
||||
if(editedSong != null) {
|
||||
editedSong.setTitle(title);
|
||||
editedSong.setComposer(composer);
|
||||
editedSong.setYear(releaseYear);
|
||||
editedSong.setGenre(genre);
|
||||
editedSong.setSyncStatus(SyncStatus.MODIFIED);
|
||||
homeViewModel.updateSong(editedSong);
|
||||
} else {
|
||||
Song song = new Song(title, composer, genre, releaseYear);
|
||||
|
||||
Context context = this.getContext();
|
||||
List<NoteSheet> noteSheetList = new ArrayList<>();
|
||||
for(Uri uri : noteSheetFiles) {
|
||||
Tupel<String, String> result = NoteSheetsUtil.saveImageInternally(context.getContentResolver(), uri, context.getFilesDir());
|
||||
noteSheetList.add(new NoteSheet(result.getValue00(), result.getValue01()));
|
||||
}
|
||||
homeViewModel.addSong(song, noteSheetList);
|
||||
}
|
||||
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
}
|
||||
|
||||
public void setNoteSheetFiles(Uri[] noteSheetFiles) {
|
||||
this.noteSheetFiles = noteSheetFiles;
|
||||
}
|
||||
|
||||
public void setHomeViewModel(HomeViewModel homeViewModel) {
|
||||
this.homeViewModel = homeViewModel;
|
||||
}
|
||||
|
||||
public void setEditedSong(Song editedSong) {
|
||||
this.editedSong = editedSong;
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package com.stormtales.notevault.utils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class NoteSheetsUtil {
|
||||
private static long getImageTimestamp(Context context, Uri imageUri) {
|
||||
try {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(imageUri);
|
||||
ExifInterface exif = new ExifInterface(inputStream);
|
||||
|
||||
String dateTime = exif.getAttribute(ExifInterface.TAG_DATETIME);
|
||||
if(dateTime != null) {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.getDefault());
|
||||
Date date = format.parse(dateTime);
|
||||
return date != null ? date.getTime() : 0;
|
||||
} else {
|
||||
File file = new File(imageUri.getPath());
|
||||
return file.lastModified();
|
||||
}
|
||||
} catch (IOException | ParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void sortNoteSheetsByTimestamp(Context context, Uri[] uris) {
|
||||
ArrayList<UriTimestamp> uriTimestamps = new ArrayList<>();
|
||||
|
||||
for(Uri uri : uris) {
|
||||
long timestamp = getImageTimestamp(context, uri);
|
||||
uriTimestamps.add(new UriTimestamp(uri, timestamp));
|
||||
}
|
||||
|
||||
Collections.sort(uriTimestamps, Comparator.comparingLong(UriTimestamp::getTimestamp));
|
||||
|
||||
for(int i=0; i<uriTimestamps.size(); i++) {
|
||||
uris[i] = uriTimestamps.get(i).getUri();
|
||||
}
|
||||
}
|
||||
|
||||
public static Tupel<String, String> saveImageInternally(ContentResolver contentResolver, Uri uri, File filesDir) {
|
||||
try (InputStream inputStream = contentResolver.openInputStream(uri)) {
|
||||
// Datei erstellen
|
||||
File imageFile = new File(filesDir, "saved_image_" + System.currentTimeMillis() + ".jpg");
|
||||
try (OutputStream outputStream = Files.newOutputStream(imageFile.toPath())) {
|
||||
// SHA-256 Hash-Funktion initialisieren
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
// Daten blockweise lesen und gleichzeitig schreiben und hashen
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
digest.update(buffer, 0, bytesRead); // Hash aktualisieren
|
||||
}
|
||||
|
||||
// Hash berechnen
|
||||
byte[] hashBytes = digest.digest();
|
||||
StringBuilder hashString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
hashString.append(String.format("%02x", b)); // Bytes in Hexadezimal umwandeln
|
||||
}
|
||||
|
||||
// Hashwert zurückgeben (kann auch zusammen mit dem Pfad gespeichert werden)
|
||||
return new Tupel<>(imageFile.getAbsolutePath(), hashString.toString());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Tupel<String, String> saveImageFromServer(ResponseBody body, File filesDir) {
|
||||
File imageFile = new File(filesDir, "saved_image_" + System.currentTimeMillis() + ".jpg");
|
||||
try (InputStream inputStream = body.byteStream();
|
||||
FileOutputStream outputStream = new FileOutputStream(imageFile)) {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
// Daten blockweise lesen und gleichzeitig schreiben und hashen
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
digest.update(buffer, 0, bytesRead); // Hash aktualisieren
|
||||
}
|
||||
|
||||
// Hash berechnen
|
||||
byte[] hashBytes = digest.digest();
|
||||
StringBuilder hashString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
hashString.append(String.format("%02x", b)); // Bytes in Hexadezimal umwandeln
|
||||
}
|
||||
|
||||
// Hashwert zurückgeben (kann auch zusammen mit dem Pfad gespeichert werden)
|
||||
return new Tupel<>(imageFile.getAbsolutePath(), hashString.toString());
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteNoteSheet(String filePath) throws IOException {
|
||||
Files.delete(new File(filePath).toPath());
|
||||
}
|
||||
}
|
33
app/src/main/java/com/stormtales/notevault/utils/Tupel.java
Normal file
33
app/src/main/java/com/stormtales/notevault/utils/Tupel.java
Normal file
@ -0,0 +1,33 @@
|
||||
package com.stormtales.notevault.utils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Tupel<A,B> {
|
||||
private final A value00;
|
||||
private final B value01;
|
||||
|
||||
public Tupel(A value00, B value01) {
|
||||
this.value00 = value00;
|
||||
this.value01 = value01;
|
||||
}
|
||||
|
||||
public A getValue00() {
|
||||
return value00;
|
||||
}
|
||||
|
||||
public B getValue01() {
|
||||
return value01;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Tupel<?, ?> tupel = (Tupel<?, ?>) o;
|
||||
return Objects.equals(value00, tupel.value00) && Objects.equals(value01, tupel.value01);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value00, value01);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.stormtales.notevault.utils;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public class UriTimestamp {
|
||||
private final Uri uri;
|
||||
private final long timestamp;
|
||||
|
||||
public UriTimestamp(Uri uri, long timestamp) {
|
||||
this.uri = uri;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package come.stormborntales.notevault
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import come.stormborntales.notevault.ui.screens.FullscreenImageViewer
|
||||
import come.stormborntales.notevault.ui.theme.NoteVaultTheme
|
||||
|
||||
// FullscreenImageViewerActivity.kt
|
||||
class FullscreenImageViewerActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val imageUris = intent.getParcelableArrayListExtra<Uri>("imageUris") ?: emptyList()
|
||||
|
||||
setContent {
|
||||
NoteVaultTheme {
|
||||
FullscreenImageViewer(
|
||||
images = imageUris,
|
||||
onClose = { finish() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
package come.stormborntales.notevault
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.NavigationDrawerItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import come.stormborntales.notevault.data.local.AppDatabase
|
||||
import come.stormborntales.notevault.data.local.entity.NoteEntity
|
||||
import come.stormborntales.notevault.data.repository.NoteRepository
|
||||
import come.stormborntales.notevault.ui.screens.AddNoteDialog
|
||||
import come.stormborntales.notevault.ui.screens.MainScreen
|
||||
import come.stormborntales.notevault.ui.screens.NotesScreen
|
||||
import come.stormborntales.notevault.ui.screens.SettingsScreen
|
||||
import come.stormborntales.notevault.ui.theme.NoteVaultTheme
|
||||
import come.stormborntales.notevault.ui.viewmodel.NoteViewModel
|
||||
import come.stormborntales.notevault.ui.viewmodel.NoteViewModelFactory
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val applicationContext = applicationContext
|
||||
val database = AppDatabase.getDatabase(applicationContext)
|
||||
val repository = NoteRepository(database.noteDao())
|
||||
val viewModelFactory = NoteViewModelFactory(repository)
|
||||
|
||||
setContent {
|
||||
NoteVaultTheme {
|
||||
val navController = rememberNavController()
|
||||
val viewModel: NoteViewModel = viewModel(factory = viewModelFactory)
|
||||
|
||||
var selectedUris by remember { mutableStateOf<List<Uri>>(emptyList()) }
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var noteToEdit by remember { mutableStateOf<NoteEntity?>(null) }
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenMultipleDocuments(),
|
||||
onResult = { uris ->
|
||||
if (uris.isNotEmpty()) {
|
||||
selectedUris = uris
|
||||
showDialog = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val openDialog: (NoteEntity?) -> Unit = { note ->
|
||||
Log.d("EditNote", "NoteEntity: ${note?.title}")
|
||||
noteToEdit = note
|
||||
showDialog = true
|
||||
}
|
||||
|
||||
val navItems = listOf(
|
||||
"Home" to "main",
|
||||
"Notes" to "notes",
|
||||
"Einstellungen" to "settings"
|
||||
)
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
Text("Navigation", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium)
|
||||
navItems.forEach { (label, route) ->
|
||||
NavigationDrawerItem(
|
||||
label = { Text(label) },
|
||||
selected = false,
|
||||
onClick = {
|
||||
navController.navigate(route) {
|
||||
popUpTo(navController.graph.startDestinationId) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
if(currentRoute == "main") {
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = {
|
||||
searchQuery = it
|
||||
viewModel.searchQuery.value = it
|
||||
},
|
||||
placeholder = { Text("Suche...") },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 48.dp), // Platz für Avatar
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
containerColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Text("NoteVault")
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
||||
Icon(Icons.Default.Menu, contentDescription = "Menü öffnen")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Box {
|
||||
IconButton(onClick = { isMenuExpanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AccountCircle, // Avatar-Icon
|
||||
contentDescription = "Benutzerprofil"
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = isMenuExpanded,
|
||||
onDismissRequest = { isMenuExpanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Profil") },
|
||||
onClick = {
|
||||
isMenuExpanded = false
|
||||
// TODO: Navigiere ggf. zu Profilscreen
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Abmelden") },
|
||||
onClick = {
|
||||
isMenuExpanded = false
|
||||
// TODO: Logout-Logik
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "main",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable("main") {
|
||||
MainScreen(
|
||||
viewModel = viewModel,
|
||||
onAddNoteClicked = {
|
||||
imagePickerLauncher.launch(arrayOf("image/*"))
|
||||
},
|
||||
onEditNote = openDialog
|
||||
)
|
||||
}
|
||||
composable("settings") {
|
||||
SettingsScreen()
|
||||
}
|
||||
composable("notes") {
|
||||
NotesScreen()
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
val context = LocalContext.current
|
||||
AddNoteDialog(
|
||||
onDismiss = { showDialog = false },
|
||||
onSave = { title, composer, year, genre, description ->
|
||||
if (noteToEdit == null) {
|
||||
viewModel.addNote(
|
||||
context = context,
|
||||
title = title,
|
||||
composer = composer,
|
||||
year = year,
|
||||
genre = genre,
|
||||
description = description,
|
||||
selectedUris = selectedUris,
|
||||
onDone = { showDialog = false }
|
||||
)
|
||||
} else {
|
||||
viewModel.updateNote(
|
||||
editedNote = noteToEdit!!,
|
||||
updatedTitle = title,
|
||||
updatedComposer = composer,
|
||||
updatedYear = year,
|
||||
updatedGenre = genre,
|
||||
updatedDescription = description,
|
||||
onDone = { showDialog = false }
|
||||
)
|
||||
}
|
||||
},
|
||||
initialTitle = noteToEdit?.title ?: "",
|
||||
initialComposer = noteToEdit?.composer,
|
||||
initialYear = noteToEdit?.year,
|
||||
initialGenre = noteToEdit?.genre,
|
||||
initialDescription = noteToEdit?.description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package come.stormborntales.notevault.data.local
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import come.stormborntales.notevault.data.local.dao.NoteDao
|
||||
import come.stormborntales.notevault.data.local.entity.NoteCollection
|
||||
import come.stormborntales.notevault.data.local.entity.NoteEntity
|
||||
|
||||
@Database(entities = [NoteEntity::class, NoteCollection::class], version = 1)
|
||||
@TypeConverters(UriListConverter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun noteDao(): NoteDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: AppDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): AppDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
AppDatabase::class.java,
|
||||
"note_database"
|
||||
).build()
|
||||
INSTANCE = instance
|
||||
Log.d("AppDatabase", "Datenbank erstellt: ${instance.openHelper.writableDatabase}")
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package come.stormborntales.notevault.data.local
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
|
||||
class UriListConverter {
|
||||
@TypeConverter
|
||||
fun fromList(list: List<String>): String = list.joinToString(",")
|
||||
|
||||
@TypeConverter
|
||||
fun toList(data: String): List<String> = if (data.isBlank()) emptyList() else data.split(",")
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package come.stormborntales.notevault.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import come.stormborntales.notevault.data.local.entity.NoteEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface NoteDao {
|
||||
@Query("SELECT * FROM notes")
|
||||
fun getAll(): Flow<List<NoteEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(note: NoteEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(note: NoteEntity)
|
||||
|
||||
@Update
|
||||
suspend fun update(note: NoteEntity)
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package come.stormborntales.notevault.data.local.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity("note_collections")
|
||||
data class NoteCollection(
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0,
|
||||
var name: String,
|
||||
var parentId: Int? = null
|
||||
)
|
@ -1,24 +0,0 @@
|
||||
package come.stormborntales.notevault.data.local.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "notes",
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = NoteCollection::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["collectionId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)])
|
||||
data class NoteEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
var title: String,
|
||||
val images: List<String>, // oder String + TypeConverter
|
||||
var composer: String?,
|
||||
var year: Int?,
|
||||
var genre: String?,
|
||||
var description: String?,
|
||||
val imagePreview: String,
|
||||
val collectionId: Int?
|
||||
)
|
@ -1,10 +0,0 @@
|
||||
package come.stormborntales.notevault.data.model
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
class NoteEntry(
|
||||
val title: String, val images: List<Uri>, val composer: String?, val year: Int?,
|
||||
val genre: String?, val description: String?
|
||||
) {
|
||||
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package come.stormborntales.notevault.data.repository
|
||||
|
||||
import come.stormborntales.notevault.data.local.dao.NoteDao
|
||||
import come.stormborntales.notevault.data.local.entity.NoteEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class NoteRepository(private val dao: NoteDao) {
|
||||
val allNotes: Flow<List<NoteEntity>> = dao.getAll()
|
||||
|
||||
suspend fun insert(note: NoteEntity) = dao.insert(note)
|
||||
suspend fun delete(note: NoteEntity) = dao.delete(note)
|
||||
|
||||
suspend fun update(note: NoteEntity) = dao.update(note);
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun AddNoteDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (title: String, composer: String?, year: Int?, genre: String?, description: String?) -> Unit,
|
||||
initialTitle: String = "",
|
||||
initialComposer: String? = null,
|
||||
initialYear: Int? = null,
|
||||
initialGenre: String? = null,
|
||||
initialDescription: String? = null
|
||||
) {
|
||||
var title by remember { mutableStateOf(initialTitle) }
|
||||
var composer by remember { mutableStateOf(initialComposer ?: "") }
|
||||
var yearText by remember { mutableStateOf(initialYear?.toString() ?: "") }
|
||||
var genre by remember { mutableStateOf(initialGenre ?: "") }
|
||||
var description by remember { mutableStateOf(initialDescription ?: "") }
|
||||
var showTitleError by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
if (title.isBlank()) {
|
||||
showTitleError = true
|
||||
} else {
|
||||
val year = yearText.toIntOrNull()
|
||||
onSave(
|
||||
title.trim(),
|
||||
composer.takeIf { it.isNotBlank() },
|
||||
year,
|
||||
genre.takeIf { it.isNotBlank() },
|
||||
description.takeIf { it.isNotBlank() }
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Text("Speichern")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text("Notenblatt hinzufügen", style = MaterialTheme.typography.titleLarge)
|
||||
},
|
||||
text = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = {
|
||||
title = it
|
||||
if (it.isNotBlank()) showTitleError = false
|
||||
},
|
||||
label = { Text("Titel*") },
|
||||
isError = showTitleError,
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
)
|
||||
if (showTitleError) {
|
||||
Text(
|
||||
"Titel darf nicht leer sein",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = composer,
|
||||
onValueChange = { composer = it },
|
||||
label = { Text("Komponist (optional)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = yearText,
|
||||
onValueChange = { yearText = it.filter { c -> c.isDigit() } },
|
||||
label = { Text("Erscheinungsjahr (optional)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = genre,
|
||||
onValueChange = { genre = it },
|
||||
label = { Text("Genre (optional)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text("Beschreibung (optional)") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
maxLines = 4
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.screens
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun FullscreenImageViewer(
|
||||
images: List<Uri>,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
val pagerState = rememberPagerState(pageCount = { images.size })
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
val context = LocalContext.current
|
||||
val imageBitmap = remember(images[page]) {
|
||||
loadImageBitmap(context, images[page].toString())
|
||||
}
|
||||
|
||||
imageBitmap?.let {
|
||||
Image(
|
||||
bitmap = it,
|
||||
contentDescription = "Notenbild",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onClose,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Close, contentDescription = "Schließen", tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,269 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageDecoder
|
||||
import android.media.Image
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import come.stormborntales.notevault.FullscreenImageViewerActivity
|
||||
import come.stormborntales.notevault.data.local.entity.NoteEntity
|
||||
import come.stormborntales.notevault.data.model.NoteEntry
|
||||
import come.stormborntales.notevault.ui.viewmodel.NoteViewModel
|
||||
import java.io.InputStream
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
fun loadImageBitmap(context: Context, uriString: String): ImageBitmap? {
|
||||
return try {
|
||||
val uri = uriString.toUri()
|
||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
bitmap?.asImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}@Composable
|
||||
fun NoteCard(note: NoteEntity, onDeleteNote: (NoteEntity) -> Unit, onEditNote: (NoteEntity) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp // Bildschirmbreite in dp
|
||||
|
||||
// Dynamische Bildgröße basierend auf der Bildschirmbreite
|
||||
val imageSize = if (screenWidth < 400) 80.dp else 120.dp
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
) {
|
||||
// Responsive Layout: Überprüfen, ob der Bildschirm schmaler als 360 dp ist
|
||||
if (screenWidth < 400) {
|
||||
// Wenn der Bildschirm schmal ist, arrangiere die Elemente vertikal
|
||||
Row(modifier = Modifier.padding(16.dp)) {
|
||||
AsyncImage(
|
||||
model = note.imagePreview,
|
||||
contentDescription = "Vorschaubild",
|
||||
modifier = Modifier
|
||||
.size(imageSize)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = note.title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
note.composer?.let {
|
||||
Text("von $it", style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
note.year?.let {
|
||||
Text("Jahr: $it", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
|
||||
note.genre?.let {
|
||||
Text("Genre: $it", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
|
||||
note.description?.let {
|
||||
Text("Beschreibung: $it", style = MaterialTheme.typography.bodySmall, maxLines = 2)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp)) // Abstand zwischen Text und Buttons
|
||||
}
|
||||
}
|
||||
// Buttons unter dem Text
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
val uris = ArrayList<Uri>()
|
||||
note.images.forEach { uris.add(it.toUri()) }
|
||||
|
||||
val intent = Intent(context, FullscreenImageViewerActivity::class.java).apply {
|
||||
putParcelableArrayListExtra("imageUris", ArrayList(uris))
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text("Anzeigen", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
onEditNote(note)
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text("Bearbeiten", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
onDeleteNote(note)
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text("Löschen", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Wenn der Bildschirm breiter als 360 dp ist, arrangiere die Elemente nebeneinander
|
||||
Row(modifier = Modifier.padding(16.dp)) {
|
||||
// Linkes Vorschaubild
|
||||
AsyncImage(
|
||||
model = note.imagePreview,
|
||||
contentDescription = "Vorschaubild",
|
||||
modifier = Modifier
|
||||
.size(imageSize)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
|
||||
// Rechte Info-Spalte
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f) // Damit die Spalte den verfügbaren Platz nutzt
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = note.title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
note.composer?.let {
|
||||
Text("von $it", style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
note.year?.let {
|
||||
Text("Jahr: $it", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
|
||||
note.genre?.let {
|
||||
Text("Genre: $it", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
|
||||
note.description?.let {
|
||||
Text("Beschreibung: $it", style = MaterialTheme.typography.bodySmall, maxLines = 2)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Buttons in einer Row anordnen
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth() // Stellt sicher, dass die Row den gesamten verfügbaren Platz einnimmt
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
val uris = ArrayList<Uri>()
|
||||
note.images.forEach { uris.add(it.toUri()) }
|
||||
|
||||
val intent = Intent(context, FullscreenImageViewerActivity::class.java).apply {
|
||||
putParcelableArrayListExtra("imageUris", ArrayList(uris))
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text("Anzeigen", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
onEditNote(note)
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text("Bearbeiten", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
onDeleteNote(note)
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text("Löschen", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
onAddNoteClicked: () -> Unit, // Übergib hier deine Logik für den Import
|
||||
viewModel: NoteViewModel,
|
||||
onEditNote: (NoteEntity) -> Unit
|
||||
) {
|
||||
val notes by viewModel.filteredNotes.collectAsState(initial = emptyList())
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = onAddNoteClicked
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Neue Noten hinzufügen")
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.End
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
contentPadding = innerPadding,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
items(notes) { note ->
|
||||
NoteCard(note = note, onDeleteNote = { noteEntity ->
|
||||
viewModel.deleteNote(noteEntity)
|
||||
}, onEditNote = onEditNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun NotesScreen() {
|
||||
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Einstellungen",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
Divider()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Beispielhafte Einstellung
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Dark Mode",
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Switch(
|
||||
checked = false,
|
||||
onCheckedChange = { /* TODO: Dark Mode aktivieren */ }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Benachrichtigungen",
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Switch(
|
||||
checked = true,
|
||||
onCheckedChange = { /* TODO: Benachrichtigungseinstellungen */ }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { /* TODO: Impressum anzeigen */ },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Impressum")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
@ -1,58 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NoteVaultTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
@ -1,134 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import come.stormborntales.notevault.data.local.entity.NoteEntity
|
||||
import come.stormborntales.notevault.data.repository.NoteRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import androidx.core.graphics.scale
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class NoteViewModel(
|
||||
private val repository: NoteRepository,
|
||||
) : ViewModel() {
|
||||
// Sucheingabe als StateFlow
|
||||
val searchQuery = MutableStateFlow("")
|
||||
|
||||
// Alle Notizen als Flow aus der Datenbank (NICHT blockierend!)
|
||||
private val allNotes: Flow<List<NoteEntity>> = repository.allNotes
|
||||
|
||||
// Gefilterte Notizen basierend auf Sucheingabe
|
||||
val filteredNotes: StateFlow<List<NoteEntity>> = combine(searchQuery, allNotes) { query, notes ->
|
||||
if (query.isBlank()) {
|
||||
notes
|
||||
} else {
|
||||
notes.filter {
|
||||
it.title.contains(query, ignoreCase = true) ||
|
||||
it.description?.contains(query, ignoreCase = true) == true
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
|
||||
fun createPreviewImage(context: Context, uri: Uri): File? {
|
||||
return try {
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
val originalBitmap = BitmapFactory.decodeStream(inputStream) ?: return null
|
||||
|
||||
val previewBitmap = originalBitmap.scale(128, 128, false)
|
||||
val previewFile = File(context.filesDir, "preview_${System.currentTimeMillis()}.jpg")
|
||||
previewFile.outputStream().use { out ->
|
||||
previewBitmap.compress(Bitmap.CompressFormat.JPEG, 75, out)
|
||||
}
|
||||
previewFile
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun addNote(context: Context, title: String, composer: String?, year: Int?, genre: String?, description: String?, selectedUris: List<Uri>, onDone:() -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val copiedUris = selectedUris.mapNotNull { uri ->
|
||||
try {
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
val extension = MimeTypeMap.getSingleton()
|
||||
.getExtensionFromMimeType(context.contentResolver.getType(uri)) ?: "jpg"
|
||||
|
||||
val outputFile = File(context.filesDir, "note_${System.currentTimeMillis()}.$extension")
|
||||
inputStream?.use { input ->
|
||||
outputFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
Log.d("NoteViewModel", "NoteEntityFile" + outputFile.absolutePath)
|
||||
Uri.fromFile(outputFile).toString() // speichern als String
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (copiedUris.isNotEmpty()) {
|
||||
val preview_image_path = createPreviewImage(context, uri = selectedUris[0])
|
||||
|
||||
val note = NoteEntity(
|
||||
title = title,
|
||||
images = copiedUris, // muss als List<String> gespeichert sein
|
||||
composer = composer,
|
||||
year = year,
|
||||
genre = genre,
|
||||
description = description,
|
||||
imagePreview = preview_image_path.toString(),
|
||||
collectionId = null
|
||||
)
|
||||
|
||||
repository.insert(note)
|
||||
}
|
||||
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteNote(note: NoteEntity) {
|
||||
viewModelScope.launch {
|
||||
repository.delete(note)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNote(
|
||||
editedNote: NoteEntity,
|
||||
updatedTitle: String,
|
||||
updatedComposer: String?,
|
||||
updatedYear: Int?,
|
||||
updatedGenre: String?,
|
||||
updatedDescription: String?,
|
||||
onDone: () -> Unit
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
editedNote.title = updatedTitle
|
||||
editedNote.year = updatedYear;
|
||||
editedNote.composer = updatedComposer
|
||||
editedNote.genre = updatedGenre
|
||||
editedNote.description = updatedDescription
|
||||
|
||||
repository.update(editedNote)
|
||||
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package come.stormborntales.notevault.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import come.stormborntales.notevault.data.repository.NoteRepository
|
||||
|
||||
class NoteViewModelFactory(
|
||||
private val repository: NoteRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(NoteViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return NoteViewModel(repository) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
9
app/src/main/res/drawable/delete.xml
Normal file
9
app/src/main/res/drawable/delete.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M280,840q-33,0 -56.5,-23.5T200,760v-520h-40v-80h200v-40h240v40h200v80h-40v520q0,33 -23.5,56.5T680,840L280,840ZM680,240L280,240v520h400v-520ZM360,680h80v-360h-80v360ZM520,680h80v-360h-80v360ZM280,240v520,-520Z"
|
||||
android:fillColor="#e8eaed"/>
|
||||
</vector>
|
6
app/src/main/res/drawable/dialog_background.xml
Normal file
6
app/src/main/res/drawable/dialog_background.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@android:color/white" />
|
||||
<corners android:radius="16dp" />
|
||||
<padding android:left="16dp" android:top="16dp" android:right="16dp" android:bottom="16dp" />
|
||||
<elevation android:height="4dp" />
|
||||
</shape>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z"
|
||||
android:fillColor="#e8eaed"/>
|
||||
</vector>
|
12
app/src/main/res/drawable/ic_menu_camera.xml
Normal file
12
app/src/main/res/drawable/ic_menu_camera.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zm3,15c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_menu_gallery.xml
Normal file
9
app/src/main/res/drawable/ic_menu_gallery.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M22,16V4c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zm-11,-4l2.03,2.71L16,11l4,5H8l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2H4V6H2z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_menu_slideshow.xml
Normal file
9
app/src/main/res/drawable/ic_menu_slideshow.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6zm16,-4H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zm-8,12.5v-9l6,4.5 -6,4.5z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/login.xml
Normal file
9
app/src/main/res/drawable/login.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M480,840v-80h280v-560L480,200v-80h280q33,0 56.5,23.5T840,200v560q0,33 -23.5,56.5T760,840L480,840ZM400,680 L345,622 447,520L120,520v-80h327L345,338l55,-58 200,200 -200,200Z"
|
||||
android:fillColor="#e8eaed"/>
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user