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