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
 | 
				
			||||||
/music_database-shm
 | 
					/music_database-shm
 | 
				
			||||||
/music_database-wal
 | 
					/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
 | 
					# Datasource local storage ignored files
 | 
				
			||||||
/dataSources/
 | 
					/dataSources/
 | 
				
			||||||
/dataSources.local.xml
 | 
					/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"?>
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
<project version="4">
 | 
					<project version="4">
 | 
				
			||||||
  <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
 | 
					  <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>
 | 
					      <driver-ref>sqlite.xerial</driver-ref>
 | 
				
			||||||
      <synchronize>true</synchronize>
 | 
					      <synchronize>true</synchronize>
 | 
				
			||||||
      <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
 | 
					      <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>
 | 
					      <jdbc-additional-properties>
 | 
				
			||||||
        <property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
 | 
					        <property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
 | 
				
			||||||
      </jdbc-additional-properties>
 | 
					      </jdbc-additional-properties>
 | 
				
			||||||
@ -19,53 +19,5 @@
 | 
				
			|||||||
        </library>
 | 
					        </library>
 | 
				
			||||||
      </libraries>
 | 
					      </libraries>
 | 
				
			||||||
    </data-source>
 | 
					    </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>
 | 
					  </component>
 | 
				
			||||||
</project>
 | 
					</project>
 | 
				
			||||||
@ -4,7 +4,7 @@
 | 
				
			|||||||
    <selectionStates>
 | 
					    <selectionStates>
 | 
				
			||||||
      <SelectionState runConfigName="app">
 | 
					      <SelectionState runConfigName="app">
 | 
				
			||||||
        <option name="selectionMode" value="DROPDOWN" />
 | 
					        <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">
 | 
					          <Target type="DEFAULT_BOOT">
 | 
				
			||||||
            <handle>
 | 
					            <handle>
 | 
				
			||||||
              <DeviceId pluginId="PhysicalDevice" identifier="serial=R52N50NLGRT" />
 | 
					              <DeviceId pluginId="PhysicalDevice" identifier="serial=R52N50NLGRT" />
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@
 | 
				
			|||||||
    <option name="linkedExternalProjectsSettings">
 | 
					    <option name="linkedExternalProjectsSettings">
 | 
				
			||||||
      <GradleProjectSettings>
 | 
					      <GradleProjectSettings>
 | 
				
			||||||
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
 | 
					        <option name="externalProjectPath" value="$PROJECT_DIR$" />
 | 
				
			||||||
 | 
					        <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
 | 
				
			||||||
        <option name="modules">
 | 
					        <option name="modules">
 | 
				
			||||||
          <set>
 | 
					          <set>
 | 
				
			||||||
            <option value="$PROJECT_DIR$" />
 | 
					            <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">
 | 
					  <component name="FrameworkDetectionExcludesConfiguration">
 | 
				
			||||||
    <file type="web" url="file://$PROJECT_DIR$" />
 | 
					    <file type="web" url="file://$PROJECT_DIR$" />
 | 
				
			||||||
  </component>
 | 
					  </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>
 | 
					</project>
 | 
				
			||||||
@ -1,17 +1,14 @@
 | 
				
			|||||||
plugins {
 | 
					plugins {
 | 
				
			||||||
    alias(libs.plugins.android.application)
 | 
					    alias(libs.plugins.android.application)
 | 
				
			||||||
    alias(libs.plugins.kotlin.android)
 | 
					 | 
				
			||||||
    alias(libs.plugins.kotlin.compose)
 | 
					 | 
				
			||||||
    alias(libs.plugins.ksp) // NEU
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
android {
 | 
					android {
 | 
				
			||||||
    namespace = "come.stormborntales.notevault"
 | 
					    namespace = "com.stormtales.notevault"
 | 
				
			||||||
    compileSdk = 34
 | 
					    compileSdk = 34
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    defaultConfig {
 | 
					    defaultConfig {
 | 
				
			||||||
        applicationId = "come.stormborntales.notevault"
 | 
					        applicationId = "com.stormtales.notevault"
 | 
				
			||||||
        minSdk = 24
 | 
					        minSdk = 29
 | 
				
			||||||
        targetSdk = 34
 | 
					        targetSdk = 34
 | 
				
			||||||
        versionCode = 1
 | 
					        versionCode = 1
 | 
				
			||||||
        versionName = "1.0"
 | 
					        versionName = "1.0"
 | 
				
			||||||
@ -29,38 +26,28 @@ android {
 | 
				
			|||||||
        sourceCompatibility = JavaVersion.VERSION_11
 | 
					        sourceCompatibility = JavaVersion.VERSION_11
 | 
				
			||||||
        targetCompatibility = JavaVersion.VERSION_11
 | 
					        targetCompatibility = JavaVersion.VERSION_11
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    kotlinOptions {
 | 
					 | 
				
			||||||
        jvmTarget = "11"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    buildFeatures {
 | 
					    buildFeatures {
 | 
				
			||||||
        compose = true
 | 
					        viewBinding = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dependencies {
 | 
					dependencies {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    implementation(libs.androidx.core.ktx)
 | 
					    implementation(libs.appcompat)
 | 
				
			||||||
    implementation(libs.androidx.lifecycle.runtime.ktx)
 | 
					    implementation(libs.material)
 | 
				
			||||||
    implementation(libs.androidx.activity.compose)
 | 
					    implementation(libs.constraintlayout)
 | 
				
			||||||
    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.lifecycle.livedata.ktx)
 | 
					    implementation(libs.lifecycle.livedata.ktx)
 | 
				
			||||||
    implementation(libs.compose.runtime.livedata)
 | 
					    implementation(libs.lifecycle.viewmodel.ktx)
 | 
				
			||||||
    implementation(libs.coil.compose)
 | 
					    implementation(libs.navigation.fragment)
 | 
				
			||||||
    ksp(libs.androidx.room.compiler)
 | 
					    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"?>
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
					<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
          xmlns:tools="http://schemas.android.com/tools">
 | 
					          xmlns:tools="http://schemas.android.com/tools">
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.INTERNET" />
 | 
				
			||||||
    <application
 | 
					    <application
 | 
				
			||||||
            android:allowBackup="true"
 | 
					            android:allowBackup="true"
 | 
				
			||||||
            android:dataExtractionRules="@xml/data_extraction_rules"
 | 
					            android:dataExtractionRules="@xml/data_extraction_rules"
 | 
				
			||||||
            android:fullBackupContent="@xml/backup_rules"
 | 
					            android:fullBackupContent="@xml/backup_rules"
 | 
				
			||||||
 | 
					            android:networkSecurityConfig="@xml/network_security_config"
 | 
				
			||||||
            android:icon="@mipmap/ic_launcher"
 | 
					            android:icon="@mipmap/ic_launcher"
 | 
				
			||||||
            android:label="@string/app_name"
 | 
					            android:label="@string/app_name"
 | 
				
			||||||
            android:roundIcon="@mipmap/ic_launcher_round"
 | 
					            android:roundIcon="@mipmap/ic_launcher_round"
 | 
				
			||||||
@ -16,15 +17,18 @@
 | 
				
			|||||||
                android:name=".MainActivity"
 | 
					                android:name=".MainActivity"
 | 
				
			||||||
                android:exported="true"
 | 
					                android:exported="true"
 | 
				
			||||||
                android:label="@string/app_name"
 | 
					                android:label="@string/app_name"
 | 
				
			||||||
                android:theme="@style/Theme.NoteVault">
 | 
					                android:theme="@style/Theme.NoteVault.NoActionBar">
 | 
				
			||||||
            <intent-filter>
 | 
					            <intent-filter>
 | 
				
			||||||
                <action android:name="android.intent.action.MAIN"/>
 | 
					                <action android:name="android.intent.action.MAIN"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <category android:name="android.intent.category.LAUNCHER"/>
 | 
					                <category android:name="android.intent.category.LAUNCHER"/>
 | 
				
			||||||
            </intent-filter>
 | 
					            </intent-filter>
 | 
				
			||||||
        </activity>
 | 
					        </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>
 | 
					    </application>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</manifest>
 | 
					</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