Bläddra i källkod

Merge branch 'release/0.13.0'

Warafear 6 månader sedan
förälder
incheckning
dbd6b0686e

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "dndtools",
-  "version": "0.12.2",
+  "version": "0.13.0",
   "scripts": {
     "ng": "ng",
     "start": "nx serve",

+ 6 - 1
src/app/character/character-creator/character-creator.component.ts

@@ -398,7 +398,12 @@ export class CharacterCreatorComponent {
       this.dataAccessor.addData(
         this.characterName,
         {
-          data: {},
+          data: {
+            companions: [],
+            allies: [],
+            enemies: [],
+            others: [],
+          },
         },
         'npcs',
       ),

+ 22 - 14
src/app/journal/journal-notes/journal-notes.component.html

@@ -1,4 +1,6 @@
 <div class="entries-list">
+  <div class="title t-0">{{ "notes.sessions" | translate }}</div>
+  <divider appearance="gold-2" style="padding: 0 1rem"></divider>
   <!-- Add Button or temporary unsaved new entry -->
   @if (isNewEntry) {
     <div class="entry active">
@@ -6,13 +8,13 @@
         @if (currentEntry.title !== "") {
           {{ currentEntry.title }}
         } @else {
-          Noch kein Titel
+          {{ "notes.noTitle" | translate }}
         }
       </div>
       <div class="entry-date">
         {{ currentEntry.created | date: "shortDate" : "" : "de" }}
       </div>
-      <div class="unsaved">Nicht gespeichert</div>
+      <div class="unsaved">{{ "notes.unsaved" | translate }}</div>
     </div>
   } @else {
     <div class="entry add-button" (click)="addEntry()">
@@ -33,14 +35,14 @@
           @if (entries[index].title !== "") {
             {{ entries[index].title }}
           } @else {
-            Kein Titel
+            {{ "notes.noTitle" | translate }}
           }
         </div>
         <div class="entry-date">
           {{ entries[index].created | date: "shortDate" : "" : "de" }}
         </div>
         @if (isInEditMode && currentEntryIndex === index) {
-          <div class="unsaved">Nicht gespeichert</div>
+          <div class="unsaved">{{ "notes.unsaved" | translate }}</div>
         }
       </div>
       <div class="control-button-wrapper">
@@ -102,19 +104,25 @@
     }
     <div class="button-row">
       @if (isInEditMode) {
-        <ui-button width="w16" (click)="saveEntry()">Speichern</ui-button>
-        <ui-button width="w16" (click)="discardEntry()">Verwerfen</ui-button>
+        <ui-button width="w16" (click)="saveEntry()">{{
+          "notes.save" | translate
+        }}</ui-button>
+        <ui-button width="w16" (click)="discardEntry()">{{
+          "notes.discard" | translate
+        }}</ui-button>
       }
-      <!-- @else {
-        <ui-button width="w16" (click)="editEntry()">Bearbeiten</ui-button>
-        <ui-button width="w16" (click)="deleteEntry()"
-          >Eintrag löschen</ui-button
-        >
-      } -->
     </div>
   </div>
 } @else {
-  <div style="margin: 5rem auto; text-align: center">
-    Noch kein Eintrag vorhanden
+  <div
+    style="
+      margin: 28rem auto;
+      text-align: center;
+      font-size: 1.5rem;
+      font-weight: 600;
+      padding-left: 15rem;
+    "
+  >
+    {{ "notes.empty" | translate }}
   </div>
 }

+ 1 - 1
src/app/journal/journal-notes/journal-notes.component.scss

@@ -11,7 +11,7 @@
   background-image: url("../../../assets/images/texture-10.jpg");
   border-right: var(--gold-3);
   box-shadow: var(--shadow);
-  padding-top: 3rem;
+  padding-top: 2rem;
 
   .entry {
     width: 15rem;

+ 2 - 2
src/app/journal/journal-notes/journal-notes.component.ts

@@ -1,4 +1,4 @@
-import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
 import { Editor } from 'ngx-editor';
 import { DateAdapter } from '@angular/material/core';
 import { JournalEntry } from 'src/interfaces/interfaces';
@@ -81,8 +81,8 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
     };
     this.isNewEntry = true;
     this.isInEditMode = true;
-    // Hightlight no entry because the placeholder is shown as active
     this.backupIndex = this.currentEntryIndex;
+    // Hightlight no entry because the placeholder is shown as active
     this.currentEntryIndex = -1;
   }
 

+ 145 - 10
src/app/journal/journal-npcs/journal-npcs.component.html

@@ -1,11 +1,146 @@
-<div
-  style="
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    width: 100%;
-  "
->
-  <img style="height: 100%" src="assets/images/npc_coming_soon.jpeg" alt="" />
+<div class="npcs-container">
+  <div class="title t-0">{{ "npcs.characters" | translate }}</div>
+  <divider appearance="gold-2" style="width: 15rem"></divider>
+  <mat-accordion class="t-1" multi>
+    @for (group of ["companions", "allies", "enemies", "others"]; track group) {
+      <mat-expansion-panel [expanded]="true" class="b-1">
+        <mat-expansion-panel-header>
+          <mat-panel-title>
+            <div class="npc-list-title">{{ "npcs." + group | translate }}</div>
+          </mat-panel-title>
+        </mat-expansion-panel-header>
+        <divider [appearance]="'gold-2'" class="b-1"></divider>
+        <!-- Add-Button -->
+        @if (isNewNpc && currentType === group) {
+          <div class="npc active">
+            <div class="npc-title">
+              @if (currentNpc.name !== "") {
+                {{ currentNpc.name }}
+              } @else {
+                {{ "notes.noName" | translate }}
+              }
+            </div>
+            <div class="unsaved">{{ "notes.unsaved" | translate }}</div>
+          </div>
+        } @else {
+          <div class="npc-wrapper" (click)="addNpc(group)">
+            <div
+              class="npc add-button"
+              [ngClass]="
+                currentType === group && isInEditMode && currentIndex === -1
+                  ? 'active'
+                  : ''
+              "
+            >
+              <img src="assets/icons/UIIcons/add.svg" />
+            </div>
+          </div>
+        }
+        <div class="column gap-05 t-1">
+          @for (npc of npcs[group]; let index = $index; track npc) {
+            <div
+              class="npc-wrapper"
+              [ngClass]="{
+                active: currentIndex === index && currentType === group,
+                'edit-mode': isInEditMode
+              }"
+            >
+              <div class="npc" (click)="selectNpc(index, group)">
+                <div class="npc-title">{{ npc.name }}</div>
+              </div>
+              <div class="control-button-wrapper">
+                <icon-button
+                  [icon]="'edit-large'"
+                  (click)="editNpc()"
+                ></icon-button>
+                <icon-button
+                  [icon]="'delete'"
+                  (click)="deleteNpc()"
+                ></icon-button>
+              </div>
+            </div>
+          }
+        </div>
+      </mat-expansion-panel>
+    }
+  </mat-accordion>
 </div>
+@if (!npcsIsEmpty) {
+  @if (isInEditMode) {
+    <div class="write-container">
+      <div>
+        <label class="write-label">{{
+          "npcs.namePlaceholder" | translate
+        }}</label>
+        <div>
+          <mat-form-field class="name-write" appearance="outline">
+            <input
+              matInput
+              [(ngModel)]="currentNpc.name"
+              [placeholder]="'npcs.namePlaceholder' | translate"
+            />
+          </mat-form-field>
+        </div>
+      </div>
+      <div class="t-2">
+        <label class="write-label">{{ "npcs.short" | translate }}</label>
+        <div class="NgxEditor__Wrapper">
+          <ngx-editor-menu [editor]="shortEditor" [toolbar]="toolbar">
+          </ngx-editor-menu>
+          <ngx-editor
+            class="short-editor"
+            [editor]="shortEditor"
+            [placeholder]="'npcs.shortPlaceholder' | translate"
+            [(ngModel)]="currentNpc.shortDescription"
+          ></ngx-editor>
+        </div>
+      </div>
+      <div class="t-2">
+        <label class="write-label">{{ "npcs.long" | translate }}</label>
+        <div class="NgxEditor__Wrapper">
+          <ngx-editor-menu [editor]="longEditor" [toolbar]="toolbar">
+          </ngx-editor-menu>
+          <ngx-editor
+            class="long-editor"
+            [editor]="longEditor"
+            [placeholder]="'npcs.longPlaceholder' | translate"
+            [(ngModel)]="currentNpc.longDescription"
+          ></ngx-editor>
+        </div>
+      </div>
+      <div class="button-row">
+        <ui-button width="w16" (click)="saveNpc()">{{
+          "npcs.save" | translate
+        }}</ui-button>
+        <ui-button width="w16" (click)="discardNpc()">{{
+          "npcs.discard" | translate
+        }}</ui-button>
+      </div>
+    </div>
+  } @else {
+    <div class="read-container">
+      <div class="title-row">
+        <div class="name-read">{{ currentNpc.name }}</div>
+        <icon-button
+          [icon]="'flip'"
+          (click)="showShortDescription = !showShortDescription"
+        ></icon-button>
+      </div>
+      <divider [appearance]="'gold-2'" class="b-1 t-1"></divider>
+      <div class="short-read">
+        <div
+          class="description"
+          [innerHTML]="
+            showShortDescription
+              ? currentNpc.shortDescription
+              : currentNpc.longDescription
+          "
+        ></div>
+      </div>
+    </div>
+  }
+} @else {
+  <div class="empty-container">
+    {{ "npcs.empty" | translate }}
+  </div>
+}

+ 206 - 0
src/app/journal/journal-npcs/journal-npcs.component.scss

@@ -0,0 +1,206 @@
+.npcs-container {
+  width: 18rem;
+  height: 100%;
+  overflow-y: auto;
+  position: fixed;
+  top: 0;
+  left: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background-image: url("../../../assets/images/texture-10.jpg");
+  border-right: var(--gold-3);
+  box-shadow: var(--shadow);
+  padding-top: 1.5rem;
+}
+
+.npc-list-title {
+  font-size: 1.5rem;
+  font-weight: 500;
+}
+
+.npc {
+  width: 15rem;
+  padding: 1rem;
+  border: var(--gold-2);
+  border-radius: 10px;
+  box-shadow: var(--shadow);
+  cursor: pointer;
+  background-image: url("../../../assets/images/texture-0.jpg");
+  &:hover {
+    background-image: url("../../../assets/images/texture-5.jpg");
+  }
+
+  .npc-title {
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    font-weight: 500;
+  }
+
+  .unsaved {
+    margin-top: 0.375rem;
+    text-align: center;
+    color: rgb(52, 33, 33);
+    font-size: 0.75rem;
+    font-weight: 600;
+  }
+
+  &.active {
+    background-image: url("../../../assets/images/texture-30.jpg") !important;
+    width: 14rem;
+  }
+}
+
+.npc-wrapper {
+  display: flex;
+  gap: 0.5rem;
+  align-items: center;
+
+  .control-button-wrapper {
+    display: none;
+    flex-direction: column;
+    gap: 0.5rem;
+  }
+
+  &.active:not(.edit-mode) {
+    .npc {
+      width: 11rem;
+      background-image: url("../../../assets/images/texture-30.jpg") !important;
+    }
+    .control-button-wrapper {
+      display: flex;
+    }
+  }
+  &.active {
+    .npc {
+      background-image: url("../../../assets/images/texture-30.jpg") !important;
+    }
+  }
+  .add-button {
+    height: 3.5rem;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    &.active {
+      background-image: url("../../../assets/images/texture-30.jpg") !important;
+    }
+  }
+}
+
+.read-container {
+  width: 800px;
+  height: calc(100vh - 6rem);
+  margin-top: 1.5rem;
+  margin-left: calc(50vw - 400px + 9rem);
+  padding: 1rem 2rem 2rem;
+  overflow: auto;
+  border-radius: 6px;
+  border: var(--gold-3);
+  background-image: url("/assets/images/texture-0.jpg");
+  box-shadow: var(--shadow);
+
+  .title-row {
+    display: flex;
+    justify-content: space-between;
+
+    icon-button {
+      width: 2rem;
+      height: 2rem;
+    }
+  }
+
+  .name-read {
+    font-size: 1.5rem;
+    font-weight: 600;
+  }
+
+  .section-name {
+    font-size: 1.25rem;
+    font-weight: 500;
+  }
+}
+
+// Write view
+
+.write-container {
+  width: 800px;
+  height: calc(100vh - 2rem);
+  margin-top: 1.5rem;
+  margin-left: calc(50vw - 400px + 9rem);
+  padding: 1rem 2rem 2rem;
+  overflow: auto;
+
+  .name-write {
+    border: var(--gold-2);
+    border-radius: 6px;
+    box-shadow: var(--shadow);
+    ::ng-deep .mat-mdc-text-field-wrapper {
+      width: 26rem !important;
+      background-image: url("../../../assets/images/texture-0.jpg") !important;
+    }
+  }
+  .button-row {
+    display: flex;
+    justify-content: space-around;
+    margin-top: 2.5rem;
+  }
+}
+
+.write-label {
+  margin: 0 0 0.25rem 0.25rem;
+  font-weight: 600;
+}
+
+.empty-container {
+  width: 800px;
+  margin-top: 45vh;
+  margin-left: calc(50vw - 5rem);
+  font-weight: 600;
+  font-size: 1.5rem;
+}
+
+// MATERIAL
+
+.mat-expansion-panel {
+  border: var(--gold-2);
+}
+
+::ng-deep .mat-expansion-panel-body {
+  padding: 0 1rem 1rem !important;
+}
+
+.mat-accordion {
+  width: 16rem;
+}
+
+// Editor
+
+.NgxEditor__Wrapper {
+  border: var(--gold-3) !important;
+  border-radius: 6px;
+  box-shadow: var(--shadow);
+}
+
+::ng-deep .NgxEditor__MenuBar {
+  background-image: url("../../../assets/images/texture-10.jpg");
+}
+
+ngx-editor {
+  &.short-editor {
+    ::ng-deep .ProseMirror {
+      height: calc((100vh - 40rem) / 2);
+      background-image: url("../../../assets/images/texture-0.jpg");
+      border-radius: 0 0 4px 4px;
+    }
+  }
+
+  &.long-editor {
+    ::ng-deep .ProseMirror {
+      height: calc((100vh - 18rem) / 2);
+      background-image: url("../../../assets/images/texture-0.jpg");
+      border-radius: 0 0 4px 4px;
+    }
+  }
+}

+ 172 - 2
src/app/journal/journal-npcs/journal-npcs.component.ts

@@ -1,8 +1,178 @@
-import { Component } from '@angular/core';
+import { Component, OnInit, OnDestroy, inject } from '@angular/core';
+import { Editor } from 'ngx-editor';
+import { Npc, Npcs } from 'src/interfaces/interfaces';
+import { DataService } from 'src/services/data/data.service';
 
 @Component({
   selector: 'app-journal-npcs',
   templateUrl: './journal-npcs.component.html',
   styleUrl: './journal-npcs.component.scss',
 })
-export class JournalNpcsComponent {}
+export class JournalNpcsComponent {
+  shortEditor: Editor = new Editor();
+  longEditor: Editor = new Editor();
+  toolbar: any = [
+    // default value
+    ['bold', 'italic'],
+    ['bullet_list'],
+    [{ heading: ['h3', 'h4', 'h5', 'h6'] }],
+  ];
+
+  public showShortDescription: boolean = false;
+
+  /** Used to show the interactale form or the display version of an entry. */
+  public isInEditMode = false;
+
+  /** The index of the currently active entry */
+  public currentIndex: number = 0;
+  private backupIndex: number = -1;
+  public currentType: string = 'companions';
+
+  /** Indicates, if the currentEntry is a newly generated one that is still not saved. */
+  public isNewNpc = false;
+  public npcsIsEmpty = true;
+
+  /**The array of JournalEntries, synched to the   */
+  public npcs: Npcs = {
+    companions: [],
+    allies: [],
+    enemies: [],
+    others: [],
+  };
+
+  /** Holds the data for the current entry */
+  public currentNpc: Npc = {
+    name: '',
+    shortDescription: '',
+    longDescription: '',
+  };
+
+  private dataService: DataService = inject(DataService);
+
+  ngOnInit(): void {
+    this.npcs = this.dataService.npcs;
+    this.searchForNextNpc();
+  }
+
+  // FUNCTIONS
+
+  public selectNpc(index: number, type: string): void {
+    this.currentIndex = index;
+    this.currentType = type;
+    this.currentNpc = this.getNpc();
+    this.isNewNpc = false;
+    this.isInEditMode = false;
+  }
+
+  public addNpc(type: string): void {
+    this.currentNpc = {
+      name: '',
+      shortDescription: '',
+      longDescription: '',
+    };
+    this.currentType = type;
+    this.isInEditMode = true;
+    this.isNewNpc = true;
+    this.backupIndex = this.currentIndex;
+    this.currentIndex = -1;
+    this.npcsIsEmpty = false;
+  }
+
+  /**
+   * Switches to the edit mode for the current entry.
+   */
+  public editNpc(): void {
+    this.isInEditMode = true;
+  }
+
+  public saveNpc(): void {
+    if (this.isNewNpc) {
+      // Prepend the new Npc
+      this.getList().unshift(this.currentNpc);
+      this.isNewNpc = false;
+      this.currentIndex = 0;
+    }
+    this.isInEditMode = false;
+    this.getList()[this.currentIndex] = this.currentNpc;
+    this.uploadNpcs();
+  }
+
+  public discardNpc(): void {
+    if (this.isNewNpc) {
+      this.currentIndex = this.backupIndex;
+      this.isNewNpc = false;
+    }
+    if (this.getList().length > 0) {
+      // Reset the currentEntry to the last saved version
+      this.currentNpc = this.getNpc();
+    } else {
+      this.searchForNextNpc();
+    }
+    this.isInEditMode = false;
+  }
+
+  /**
+   * Delets an npc in the current list at the current index and uploads the result to the server.
+   */
+  public deleteNpc(): void {
+    this.getList().splice(this.currentIndex, 1);
+    if (this.getList().length === 0) {
+      this.searchForNextNpc();
+    } else {
+      this.currentIndex = Math.max(this.currentIndex - 1, 0);
+      this.currentNpc = this.getNpc();
+    }
+    this.uploadNpcs();
+  }
+
+  // Utility functions
+
+  private getList(): Npc[] {
+    switch (this.currentType) {
+      case 'companions':
+        return this.npcs.companions;
+      case 'allies':
+        return this.npcs.allies;
+      case 'enemies':
+        return this.npcs.enemies;
+      case 'others':
+        return this.npcs.others;
+      default:
+        return this.npcs.allies;
+    }
+  }
+
+  public getNpc(): Npc {
+    switch (this.currentType) {
+      case 'companions':
+        return JSON.parse(
+          JSON.stringify(this.npcs.companions[this.currentIndex]),
+        );
+      case 'allies':
+        return JSON.parse(JSON.stringify(this.npcs.allies[this.currentIndex]));
+      case 'enemies':
+        return JSON.parse(JSON.stringify(this.npcs.enemies[this.currentIndex]));
+      case 'others':
+        return JSON.parse(JSON.stringify(this.npcs.others[this.currentIndex]));
+      default:
+        return JSON.parse(JSON.stringify(this.npcs.allies[this.currentIndex]));
+    }
+  }
+
+  private searchForNextNpc(): void {
+    let notFound = true;
+    ['companions', 'allies', 'enemies', 'others'].forEach((type) => {
+      if (this.npcs[type].length > 0 && notFound) {
+        this.currentType = type;
+        this.currentIndex = 0;
+        this.currentNpc = this.getNpc();
+        notFound = false;
+      }
+    });
+    this.npcsIsEmpty = notFound;
+  }
+
+  private uploadNpcs(): void {
+    this.dataService.npcs = this.npcs;
+  }
+}

+ 5 - 5
src/app/journal/journal-spellbook/journal-spellbook.component.scss

@@ -62,15 +62,15 @@
 .icon-active {
   opacity: 1 !important;
   &::after {
-    border: 2px solid #b0826b;
+    border: 3px solid var(--gold-dark);
     content: "";
-    height: 78px;
-    width: 78px;
+    height: 80px;
+    width: 80px;
     display: block;
     border-radius: 50%;
     position: relative;
-    top: -4px;
-    left: -4px;
+    top: -5px;
+    left: -5px;
   }
 }
 

+ 7 - 4
src/app/journal/journal.module.ts

@@ -7,6 +7,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
 import { ReactiveFormsModule } from '@angular/forms';
 import { MarkdownModule } from 'ngx-markdown';
 import { NgxEditorModule } from 'ngx-editor';
+import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
 import { TranslateModule, TranslatePipe } from '@ngx-translate/core';
 
 // Material Design
@@ -18,11 +19,10 @@ import { MatTabsModule } from '@angular/material/tabs';
 import { MatExpansionModule } from '@angular/material/expansion';
 import { MatRadioModule } from '@angular/material/radio';
 import { MatDividerModule } from '@angular/material/divider';
-import {
-  MatCalendarCellClassFunction,
-  MatDatepickerModule,
-} from '@angular/material/datepicker';
+import { MatDatepickerModule } from '@angular/material/datepicker';
 import { MatNativeDateModule } from '@angular/material/core';
+import { MatButtonToggleModule } from '@angular/material/button-toggle';
+import { MatIconModule } from '@angular/material/icon';
 
 import { JournalRoutingModule } from './journal-routing.module';
 import { JournalHomeComponent } from './journal-home/journal-home.component';
@@ -202,6 +202,9 @@ import { DividerComponent } from '../shared-components/divider/divider.component
     DividerComponent,
     MatDatepickerModule,
     MatNativeDateModule,
+    NgbCollapseModule,
+    MatButtonToggleModule,
+    MatIconModule,
   ],
 })
 export class JournalModule {}

+ 24 - 4
src/assets/i18n/de.json

@@ -643,7 +643,7 @@
     "notes": "Notizen",
     "spellbook": "Zauberbuch",
     "quests": "Aufträge",
-    "npcs": "NPCs",
+    "npcs": "Personen",
     "places": "Orte",
     "maps": "Karten",
     "rules": "Regeln",
@@ -822,9 +822,29 @@
   },
   "notes": {
     "title": "Titel",
-    "placeholder": "Hier die Notizen einfügen"
+    "placeholder": "Hier die Notizen einfügen",
+    "save": "Speichern",
+    "discard": "Verwerfen",
+    "sessions": "Sessions",
+    "noName": "Noch kein Name",
+    "unsaved": "Nicht gespeichert",
+    "empty": "Noch kein Eintrag vorhanden"
+  },
+  "npcs": {
+    "characters": "Personen",
+    "companions": "Begleiter",
+    "enemies": "Feinde",
+    "allies": "Verbündete",
+    "others": "Andere",
+    "shortPlaceholder": "Kurze Beschreibung",
+    "longPlaceholder": "Ausführliche Beschreibung",
+    "namePlaceholder": "Name",
+    "short": "Kurzbeschreibung",
+    "long": "Ausführliche Beschreibung",
+    "empty": "Noch keine Personen hinzugefügt",
+    "save": "Speichern",
+    "discard": "Verwerfen"
   },
-
   "creator": {
     "new": "Neuen Charakter erstellen",
     "name": "Name",
@@ -840,6 +860,6 @@
     "hint": "Die App befindet sich immer noch in einem Entwicklungsstadium und es können Fehler auftreten",
     "issues": "<p>Fehler und Anmerkungen bitte auf dem <a href='https://gogs.koljastrohm-games.com/Warafear/DNDTools/issues'>Git-Server in Issues</a> vermerken.<p>",
     "okay": "Verstanden",
-    "version": "0.12.2"
+    "version": "0.13.0"
   }
 }

+ 24 - 3
src/assets/i18n/en.json

@@ -638,7 +638,7 @@
     "notes": "Notes",
     "spellbook": "Spellbook",
     "quests": "Quests",
-    "npcs": "NPCs",
+    "npcs": "Persons",
     "places": "Places",
     "maps": "Maps",
     "rules": "Rules",
@@ -817,7 +817,28 @@
   },
   "notes": {
     "title": "Title",
-    "placeholder": "Add notes here"
+    "placeholder": "Add notes here",
+    "save": "Save",
+    "discard": "Discard",
+    "sessions": "Sessions",
+    "noName": "No Name yet",
+    "unsaved": "Not saved",
+    "empty": "No entries added yet"
+  },
+  "npcs": {
+    "characters": "Persons",
+    "companions": "Companions",
+    "enemies": "Enemies",
+    "allies": "Allies",
+    "others": "Others",
+    "shortPlaceholder": "Short description",
+    "longPlaceholder": "Detailed description",
+    "namePlaceholder": "Name",
+    "short": "Short Description",
+    "long": "Detailed Description",
+    "empty": "No persons added yet",
+    "save": "Save",
+    "discard": "Discard"
   },
   "creator": {
     "new": "Create New Character",
@@ -834,7 +855,7 @@
     "hint": "The app is still in a development stage and errors may occur",
     "issues": "<p>Please note errors and comments on the <a href='https://gogs.koljastrohm-games.com/Warafear/DNDTools/issues'>Git server in Issues</a>.<p>",
     "okay": "Understood",
-    "version": "0.12.2",
+    "version": "0.13.0",
     "test": "Test"
   }
 }

+ 1 - 1
src/assets/icons/UIIcons/edit.svg

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 -960 960 960" ><path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/></svg>

+ 11 - 0
src/assets/icons/UIIcons/flip-2.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+
<g data-name="Layer 2">
+
<g data-name="flip-in">
+
<rect width="24" height="24" transform="rotate(-90 12 12)" opacity="0"/>
+
<path d="M5 6.09v12l-1.29-1.3a1 1 0 0 0-1.42 1.42l3 3a1 1 0 0 0 1.42 0l3-3a1 1 0 0 0 0-1.42 1 1 0 0 0-1.42 0L7 18.09v-12A1.56 1.56 0 0 1 8.53 4.5H11a1 1 0 0 0 0-2H8.53A3.56 3.56 0 0 0 5 6.09z"/>
+
<path d="M14.29 5.79a1 1 0 0 0 1.42 1.42L17 5.91v12a1.56 1.56 0 0 1-1.53 1.59H13a1 1 0 0 0 0 2h2.47A3.56 3.56 0 0 0 19 17.91v-12l1.29 1.3a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42l-3-3a1 1 0 0 0-1.42 0z"/>
+
</g>
+
</g>
+
</svg>

+ 1 - 0
src/assets/icons/UIIcons/flip.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"   viewBox="0 0 1024 1024"><path d="M213.333333 259.84v512l-55.04-55.466667a42.666667 42.666667 0 0 0-60.586666 60.586667l128 128a42.666667 42.666667 0 0 0 60.586666 0l128-128a42.666667 42.666667 0 0 0 0-60.586667 42.666667 42.666667 0 0 0-60.586666 0L298.666667 771.84v-512A66.56 66.56 0 0 1 363.946667 192H469.333333a42.666667 42.666667 0 0 0 0-85.333333H363.946667A151.893333 151.893333 0 0 0 213.333333 259.84zM609.706667 247.04a42.666667 42.666667 0 0 0 60.586666 60.586667L725.333333 252.16v512a66.56 66.56 0 0 1-65.28 67.84H554.666667a42.666667 42.666667 0 0 0 0 85.333333h105.386666A151.893333 151.893333 0 0 0 810.666667 764.16v-512l55.04 55.466667a42.666667 42.666667 0 0 0 60.586666 0 42.666667 42.666667 0 0 0 0-60.586667l-128-128a42.666667 42.666667 0 0 0-60.586666 0z"  /></svg>

+ 16 - 1
src/interfaces/interfaces.ts

@@ -166,7 +166,7 @@ export interface Spell {
 }
 // #endregion
 
-// #region Notes
+// #region Journal
 
 export interface JournalEntry {
   title: string;
@@ -175,3 +175,18 @@ export interface JournalEntry {
   startDate?: string;
   endDate?: string;
 }
+
+export interface Npcs {
+  [key: string]: Npc[];
+  companions: Npc[];
+  allies: Npc[];
+  enemies: Npc[];
+  others: Npc[];
+}
+
+export interface Npc {
+  name: string;
+  longDescription: string;
+  shortDescription: string;
+  organization?: string;
+}

+ 34 - 1
src/services/data/data.service.ts

@@ -11,6 +11,7 @@ import {
   Skill,
   Attribute,
   JournalEntry,
+  Npcs,
 } from 'src/interfaces/interfaces';
 import { SpellsService } from '../spells/spells.service';
 
@@ -149,12 +150,28 @@ export class DataService {
 
     // Notes
 
+    // Migration
     if (!Array.isArray(notesData.data)) {
       this.notesData = [];
       console.log('migrated notes data to array');
     } else {
       this.notesData = notesData.data;
     }
+
+    // NPCs
+
+    // Migration
+
+    if (npcsData.hasOwnProperty('companions')) {
+      this.npcs = npcsData;
+    } else {
+      this.npcs = {
+        companions: [],
+        allies: [],
+        enemies: [],
+        others: [],
+      };
+    }
   }
 
   // #endregion
@@ -1083,7 +1100,7 @@ export class DataService {
 
   // #endregion
 
-  // #region Notes
+  // #region Notes and Entries
 
   private _notesData: JournalEntry[] = [];
 
@@ -1096,6 +1113,22 @@ export class DataService {
     this.setData('notes', { data: newValue });
   }
 
+  private _npcsData: Npcs = {
+    companions: [],
+    allies: [],
+    enemies: [],
+    others: [],
+  };
+
+  public get npcs(): Npcs {
+    return this._npcsData;
+  }
+
+  public set npcs(newValue: Npcs) {
+    this._npcsData = newValue;
+    this.setData('npcs', newValue);
+  }
+
   // #endregion
 
   // #region database calls