Compare commits

..

No commits in common. "master" and "notevault-3" have entirely different histories.

158 changed files with 4661 additions and 1486 deletions

46
.gitea/workflows/job.yaml Normal file
View 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
View File

@ -16,6 +16,3 @@ local.properties
/music_database
/music_database-shm
/music_database-wal
/note_database
/note_database-shm
/note_database-wal

1
.idea/.gitignore vendored
View File

@ -6,4 +6,3 @@
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
/AndroidProjectSystem.xml

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="music_database" uuid="95a3c2ec-2c29-4336-900a-3993de90ae66">
<data-source source="LOCAL" name="music_database" uuid="eb23f694-6586-450b-8f6f-a75731d36b96">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/.cache/JetBrains/IntelliJIdea2025.1/device-explorer/samsung SM-P610/_/data/data/com.stormtales.notevault/databases/music_database</jdbc-url>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/music_database</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
@ -19,53 +19,5 @@
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="note_database" uuid="b3770d7c-0a73-40c6-aab8-010effaa19b6">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/.cache/JetBrains/IntelliJIdea2025.1/device-explorer/samsung SM-P610/_/data/data/come.stormborntales.notevault/databases/note_database</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="note_database [2]" uuid="ad94cfd9-e485-4151-8bf4-a080c51fa27c">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/note_database</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

View File

@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-03T08:34:29.354537334Z">
<DropdownSelection timestamp="2025-04-10T18:07:32.446414465Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=R52N50NLGRT" />

View File

@ -5,6 +5,7 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -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>

View File

@ -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>

View File

@ -3,5 +3,10 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -1,17 +1,14 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp) // NEU
}
android {
namespace = "come.stormborntales.notevault"
namespace = "com.stormtales.notevault"
compileSdk = 34
defaultConfig {
applicationId = "come.stormborntales.notevault"
minSdk = 24
applicationId = "com.stormtales.notevault"
minSdk = 29
targetSdk = 34
versionCode = 1
versionName = "1.0"
@ -29,38 +26,28 @@ android {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
viewBinding = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.androidx.foundation)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraintlayout)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.compose.runtime.livedata)
implementation(libs.coil.compose)
ksp(libs.androidx.room.compiler)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.navigation.fragment)
implementation(libs.navigation.ui)
implementation(libs.room.runtime)
implementation(libs.annotation)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
annotationProcessor(libs.room.compiler)
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
}

View File

@ -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());
}
}

View File

@ -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)
}
}

View File

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
@ -16,15 +17,18 @@
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.NoteVault">
android:theme="@style/Theme.NoteVault.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".FullscreenImageViewerActivity" />
<activity android:name=".ui.sheetdisplay.NoteSheetDisplayActivity"
android:theme="@style/Theme.AppCompat.NoActionBar">
<!-- Intent Filter hinzufügen, wenn Activity vom Launcher oder externen Apps aufgerufen werden soll -->
</activity>
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

View 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");
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
});
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
package com.stormtales.notevault.data.sync;
public enum SyncStatus {
CREATED,
DELETED,
MODIFIED,
REMOTE_MODIFIED,
SYNCED;
}

View File

@ -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();
}
}

View File

@ -0,0 +1,6 @@
package com.stormtales.notevault.network;
public interface APICallback {
void onSuccess();
void onError(String error);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
});
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View 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);
}
}

View File

@ -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;
}
}

View File

@ -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() }
)
}
}
}
}

View File

@ -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
)
}
}
}
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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(",")
}

View File

@ -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)
}

View File

@ -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
)

View File

@ -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?
)

View File

@ -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?
) {
}

View File

@ -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);
}

View File

@ -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
)
}
}
)
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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() {
}

View File

@ -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")
}
}
}

View File

@ -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)

View File

@ -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
)
}

View File

@ -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
)
*/
)

View File

@ -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()
}
}
}

View File

@ -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")
}
}

View 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>

View 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>

View 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="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>

View 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>

View 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>

View 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>

View 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