Compare commits
No commits in common. "master" and "nonx" have entirely different histories.
|
|
@ -2,6 +2,13 @@
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "ng serve",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: start",
|
||||||
|
"url": "http://localhost:4200/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
|
@ -21,8 +28,9 @@
|
||||||
"name": "Debug NestJS API",
|
"name": "Debug NestJS API",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "/home/aknuth/.nvm/versions/node/v22.14.0/bin/npx",
|
"runtimeExecutable": "npx",
|
||||||
"runtimeArgs": ["nest", "start", "--debug", "--watch"],
|
"runtimeArgs": ["nest", "start", "--debug", "--watch"],
|
||||||
|
"port": 9229,
|
||||||
"cwd": "${workspaceFolder}/api",
|
"cwd": "${workspaceFolder}/api",
|
||||||
"envFile": "${workspaceFolder}/api/.env",
|
"envFile": "${workspaceFolder}/api/.env",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { defineConfig } from 'drizzle-kit';
|
|
||||||
export default defineConfig({
|
|
||||||
dialect: 'postgresql', // 'mysql' | 'sqlite' | 'turso'
|
|
||||||
schema: './src/db/schema.ts',
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { relations } from 'drizzle-orm';
|
|
||||||
import * as t from 'drizzle-orm/pg-core';
|
import * as t from 'drizzle-orm/pg-core';
|
||||||
import { pgEnum, pgTable as table } from 'drizzle-orm/pg-core';
|
import { pgEnum, pgTable as table } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
|
@ -25,75 +24,10 @@ export const deck = table(
|
||||||
inserted: t.timestamp('inserted', { mode: 'date' }).defaultNow(),
|
inserted: t.timestamp('inserted', { mode: 'date' }).defaultNow(),
|
||||||
updated: t.timestamp('updated', { mode: 'date' }).defaultNow(),
|
updated: t.timestamp('updated', { mode: 'date' }).defaultNow(),
|
||||||
},
|
},
|
||||||
(table) => [t.uniqueIndex('deck_idx').on(table.id)],
|
table => [t.uniqueIndex('deck_idx').on(table.id)],
|
||||||
);
|
);
|
||||||
export type InsertDeck = typeof deck.$inferInsert;
|
export type InsertDeck = typeof deck.$inferInsert;
|
||||||
export type SelectDeck = typeof deck.$inferSelect;
|
export type SelectDeck = typeof deck.$inferSelect;
|
||||||
|
|
||||||
export const decks_table = table('decks', {
|
|
||||||
id: t.integer('id').primaryKey().generatedAlwaysAsIdentity(),
|
|
||||||
name: t.varchar('name').notNull(),
|
|
||||||
boxOrder: t.varchar('box_order').notNull().default('shuffle'),
|
|
||||||
user: t.varchar('user').notNull(),
|
|
||||||
inserted: t.timestamp('inserted', { mode: 'date' }).defaultNow(),
|
|
||||||
updated: t.timestamp('updated', { mode: 'date' }).defaultNow(),
|
|
||||||
});
|
|
||||||
export type InsertDecks = typeof decks_table.$inferInsert;
|
|
||||||
export type SelectDecks = typeof decks_table.$inferSelect;
|
|
||||||
|
|
||||||
export const images_table = table('images', {
|
|
||||||
id: t.integer('id').primaryKey().generatedAlwaysAsIdentity(),
|
|
||||||
deckId: t.integer('deck_id').references(() => decks_table.id),
|
|
||||||
name: t.varchar('name').notNull(),
|
|
||||||
bildid: t.varchar('bildid').notNull(),
|
|
||||||
inserted: t.timestamp('inserted', { mode: 'date' }).defaultNow(),
|
|
||||||
updated: t.timestamp('updated', { mode: 'date' }).defaultNow(),
|
|
||||||
});
|
|
||||||
export type InsertImages = typeof images_table.$inferInsert;
|
|
||||||
export type SelectImages = typeof images_table.$inferSelect;
|
|
||||||
|
|
||||||
export const boxes_table = table('boxes', {
|
|
||||||
id: t.integer('id').primaryKey().generatedAlwaysAsIdentity(),
|
|
||||||
imageId: t.integer('image_id').references(() => images_table.id),
|
|
||||||
x1: t.real('x1').notNull(),
|
|
||||||
x2: t.real('x2').notNull(),
|
|
||||||
y1: t.real('y1').notNull(),
|
|
||||||
y2: t.real('y2').notNull(),
|
|
||||||
due: t.integer('due'),
|
|
||||||
ivl: t.real('ivl'),
|
|
||||||
factor: t.real('factor'),
|
|
||||||
reps: t.integer('reps'),
|
|
||||||
lapses: t.integer('lapses'),
|
|
||||||
isGraduated: t.integer('is_graduated').notNull().default(0),
|
|
||||||
inserted: t.timestamp('inserted', { mode: 'date' }).defaultNow(),
|
|
||||||
updated: t.timestamp('updated', { mode: 'date' }).defaultNow(),
|
|
||||||
});
|
|
||||||
export type InsertBoxes = typeof boxes_table.$inferInsert;
|
|
||||||
export type SelectBoxes = typeof boxes_table.$inferSelect;
|
|
||||||
|
|
||||||
// Relations (optional, aber hilfreich für Abfragen)
|
|
||||||
export const decksRelations = relations(decks_table, ({ many }) => ({
|
|
||||||
images: many(images_table),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const imagesRelations = relations(images_table, ({ one, many }) => ({
|
|
||||||
deck: one(decks_table, {
|
|
||||||
fields: [images_table.deckId],
|
|
||||||
references: [decks_table.id],
|
|
||||||
}),
|
|
||||||
boxes: many(boxes_table),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const boxesRelations = relations(boxes_table, ({ one }) => ({
|
|
||||||
image: one(images_table, {
|
|
||||||
fields: [boxes_table.imageId],
|
|
||||||
references: [images_table.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// -------------------------------------
|
|
||||||
// USERS
|
|
||||||
// -------------------------------------
|
|
||||||
export const users = table(
|
export const users = table(
|
||||||
'users',
|
'users',
|
||||||
{
|
{
|
||||||
|
|
@ -105,7 +39,7 @@ export const users = table(
|
||||||
lastLogin: t.timestamp('lastLogin', { mode: 'date' }).defaultNow(),
|
lastLogin: t.timestamp('lastLogin', { mode: 'date' }).defaultNow(),
|
||||||
numberOfLogins: t.integer('numberOfLogins').default(1), // Neue Spalte
|
numberOfLogins: t.integer('numberOfLogins').default(1), // Neue Spalte
|
||||||
},
|
},
|
||||||
(table) => [t.uniqueIndex('users_idx').on(table.id)],
|
table => [t.uniqueIndex('users_idx').on(table.id)],
|
||||||
);
|
);
|
||||||
export type InsertUser = typeof users.$inferInsert;
|
export type InsertUser = typeof users.$inferInsert;
|
||||||
export type SelectUser = typeof users.$inferSelect;
|
export type SelectUser = typeof users.$inferSelect;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,36 @@ export class DecksController {
|
||||||
async getDecks(@Request() req) {
|
async getDecks(@Request() req) {
|
||||||
const user: User = req['user'];
|
const user: User = req['user'];
|
||||||
const entries = await this.drizzleService.getDecks(user);
|
const entries = await this.drizzleService.getDecks(user);
|
||||||
return entries;
|
const decks = {};
|
||||||
|
for (const entry of entries) {
|
||||||
|
const deckname = entry.deckname!!;
|
||||||
|
if (!decks[deckname]) {
|
||||||
|
decks[deckname] = {
|
||||||
|
name: deckname,
|
||||||
|
images: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (entry.bildname && entry.bildid) {
|
||||||
|
decks[deckname].images.push({
|
||||||
|
name: entry.bildname,
|
||||||
|
bildid: entry.bildid,
|
||||||
|
id: entry.id,
|
||||||
|
x1: entry.x1,
|
||||||
|
x2: entry.x2,
|
||||||
|
y1: entry.y1,
|
||||||
|
y2: entry.y2,
|
||||||
|
due: entry.due,
|
||||||
|
ivl: entry.ivl,
|
||||||
|
factor: entry.factor,
|
||||||
|
reps: entry.reps,
|
||||||
|
lapses: entry.lapses,
|
||||||
|
isGraduated: Boolean(entry.isGraduated),
|
||||||
|
inserted: new Date(entry.inserted!!),
|
||||||
|
updated: new Date(entry.updated!!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.values(decks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
|
@ -37,6 +66,43 @@ export class DecksController {
|
||||||
return this.drizzleService.createDeck(data.deckname, user);
|
return this.drizzleService.createDeck(data.deckname, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':deckname')
|
||||||
|
async getDeck(@Request() req, @Param('deckname') deckname: string) {
|
||||||
|
const user: User = req['user'];
|
||||||
|
const entries = await this.drizzleService.getDeckByName(deckname, user);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deck = {
|
||||||
|
name: deckname,
|
||||||
|
images: [] as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.bildname && entry.bildid) {
|
||||||
|
deck.images.push({
|
||||||
|
name: entry.bildname,
|
||||||
|
bildid: entry.bildid,
|
||||||
|
id: entry.id,
|
||||||
|
x1: entry.x1,
|
||||||
|
x2: entry.x2,
|
||||||
|
y1: entry.y1,
|
||||||
|
y2: entry.y2,
|
||||||
|
due: entry.due,
|
||||||
|
ivl: entry.ivl,
|
||||||
|
factor: entry.factor,
|
||||||
|
reps: entry.reps,
|
||||||
|
lapses: entry.lapses,
|
||||||
|
isGraduated: Boolean(entry.isGraduated),
|
||||||
|
inserted: new Date(entry.inserted!!),
|
||||||
|
updated: new Date(entry.updated!!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deck;
|
||||||
|
}
|
||||||
|
|
||||||
@Delete(':deckname')
|
@Delete(':deckname')
|
||||||
async deleteDeck(@Request() req, @Param('deckname') deckname: string) {
|
async deleteDeck(@Request() req, @Param('deckname') deckname: string) {
|
||||||
const user: User = req['user'];
|
const user: User = req['user'];
|
||||||
|
|
@ -56,21 +122,9 @@ export class DecksController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const user: User = req['user'];
|
const user: User = req['user'];
|
||||||
return this.drizzleService.updateDeck(
|
return this.drizzleService.renameDeck(oldDeckname, data.newDeckName, user);
|
||||||
oldDeckname,
|
|
||||||
{ newName: data.newDeckName },
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@Put(':oldDeckname/update')
|
|
||||||
async updateDeck(
|
|
||||||
@Request() req,
|
|
||||||
@Param('oldDeckname') oldDeckname: string,
|
|
||||||
@Body() data: { newDeckName: string; boxOrder?: 'shuffle' | 'position' },
|
|
||||||
) {
|
|
||||||
const user: User = req['user'];
|
|
||||||
return this.drizzleService.updateDeck(oldDeckname, data, user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('image')
|
@Post('image')
|
||||||
async updateImage(@Request() req, @Body() data: any) {
|
async updateImage(@Request() req, @Body() data: any) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -76,58 +76,58 @@ export class ProxyController {
|
||||||
// --------------------
|
// --------------------
|
||||||
// Cleanup Endpoint
|
// Cleanup Endpoint
|
||||||
// --------------------
|
// --------------------
|
||||||
// @Post('cleanup')
|
@Post('cleanup')
|
||||||
// async cleanupEndpoint(
|
async cleanupEndpoint(
|
||||||
// @Body() data: { dryrun?: boolean },
|
@Body() data: { dryrun?: boolean },
|
||||||
// @Res() res: express.Response,
|
@Res() res: express.Response,
|
||||||
// ) {
|
) {
|
||||||
// try {
|
try {
|
||||||
// const user = res.req['user']; // Benutzerinformationen aus dem Request
|
const user = res.req['user']; // Benutzerinformationen aus dem Request
|
||||||
|
|
||||||
// // 2. Nur Benutzer mit der spezifischen E-Mail dürfen fortfahren
|
// 2. Nur Benutzer mit der spezifischen E-Mail dürfen fortfahren
|
||||||
// if (user.email !== 'andreas.knuth@gmail.com') {
|
if (user.email !== 'andreas.knuth@gmail.com') {
|
||||||
// throw new HttpException('Zugriff verweigert.', HttpStatus.FORBIDDEN);
|
throw new HttpException('Zugriff verweigert.', HttpStatus.FORBIDDEN);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // 1. Abrufen der distinct bildid aus der Datenbank
|
// 1. Abrufen der distinct bildid aus der Datenbank
|
||||||
// const usedIds = await this.drizzleService.getDistinctBildIds(user);
|
const usedIds = await this.drizzleService.getDistinctBildIds(user);
|
||||||
// // 3. Verarbeitung des dryrun Parameters
|
// 3. Verarbeitung des dryrun Parameters
|
||||||
// const dryrun = data.dryrun !== undefined ? data.dryrun : true;
|
const dryrun = data.dryrun !== undefined ? data.dryrun : true;
|
||||||
// if (typeof dryrun !== 'boolean') {
|
if (typeof dryrun !== 'boolean') {
|
||||||
// throw new HttpException(
|
throw new HttpException(
|
||||||
// "'dryrun' muss ein boolescher Wert sein.",
|
"'dryrun' muss ein boolescher Wert sein.",
|
||||||
// HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // 4. Aufruf des Flask-Backend-Endpunkts
|
// 4. Aufruf des Flask-Backend-Endpunkts
|
||||||
// const response = await fetch('http://localhost:5000/api/cleanup', {
|
const response = await fetch('http://localhost:5000/api/cleanup', {
|
||||||
// method: 'POST',
|
method: 'POST',
|
||||||
// headers: {
|
headers: {
|
||||||
// 'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
// },
|
},
|
||||||
// body: JSON.stringify({ dryrun, usedIds }),
|
body: JSON.stringify({ dryrun, usedIds }),
|
||||||
// });
|
});
|
||||||
|
|
||||||
// const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
// if (!response.ok) {
|
if (!response.ok) {
|
||||||
// throw new HttpException(
|
throw new HttpException(
|
||||||
// result.error || 'Cleanup failed',
|
result.error || 'Cleanup failed',
|
||||||
// response.status,
|
response.status,
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // 5. Rückgabe der Ergebnisse an den Client
|
// 5. Rückgabe der Ergebnisse an den Client
|
||||||
// return res.status(HttpStatus.OK).json(result);
|
return res.status(HttpStatus.OK).json(result);
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// if (error instanceof HttpException) {
|
if (error instanceof HttpException) {
|
||||||
// throw error;
|
throw error;
|
||||||
// }
|
}
|
||||||
// throw new HttpException(
|
throw new HttpException(
|
||||||
// 'Interner Serverfehler',
|
'Interner Serverfehler',
|
||||||
// HttpStatus.INTERNAL_SERVER_ERROR,
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@
|
||||||
"@angular/compiler-cli": "^19.1.0",
|
"@angular/compiler-cli": "^19.1.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"http-server": "^14.1.1",
|
|
||||||
"jasmine-core": "~5.5.0",
|
"jasmine-core": "~5.5.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
|
@ -6984,16 +6983,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/async": {
|
|
||||||
"version": "2.6.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
|
||||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"lodash": "^4.17.14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/async-retry": {
|
"node_modules/async-retry": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
|
||||||
|
|
@ -7166,26 +7155,6 @@
|
||||||
"node": "^4.5.0 || >= 5.9"
|
"node": "^4.5.0 || >= 5.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/basic-auth": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "5.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/basic-auth/node_modules/safe-buffer": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/batch": {
|
"node_modules/batch": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||||
|
|
@ -8225,16 +8194,6 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/corser": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
|
||||||
|
|
@ -10321,16 +10280,6 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/he": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"he": "bin/he"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hosted-git-info": {
|
"node_modules/hosted-git-info": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz",
|
||||||
|
|
@ -10401,8 +10350,8 @@
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||||
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-encoding": "^2.0.0"
|
"whatwg-encoding": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -10548,47 +10497,6 @@
|
||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/http-server": {
|
|
||||||
"version": "14.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
|
|
||||||
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"basic-auth": "^2.0.1",
|
|
||||||
"chalk": "^4.1.2",
|
|
||||||
"corser": "^2.0.1",
|
|
||||||
"he": "^1.2.0",
|
|
||||||
"html-encoding-sniffer": "^3.0.0",
|
|
||||||
"http-proxy": "^1.18.1",
|
|
||||||
"mime": "^1.6.0",
|
|
||||||
"minimist": "^1.2.6",
|
|
||||||
"opener": "^1.5.1",
|
|
||||||
"portfinder": "^1.0.28",
|
|
||||||
"secure-compare": "3.0.1",
|
|
||||||
"union": "~0.5.0",
|
|
||||||
"url-join": "^4.0.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"http-server": "bin/http-server"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-server/node_modules/mime": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"mime": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/https-proxy-agent": {
|
"node_modules/https-proxy-agent": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
|
@ -13796,16 +13704,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/opener": {
|
|
||||||
"version": "1.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
|
||||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "(WTFPL OR MIT)",
|
|
||||||
"bin": {
|
|
||||||
"opener": "bin/opener-bin.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ora": {
|
"node_modules/ora": {
|
||||||
"version": "5.4.1",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
|
||||||
|
|
@ -14261,31 +14159,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/portfinder": {
|
|
||||||
"version": "1.0.33",
|
|
||||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.33.tgz",
|
|
||||||
"integrity": "sha512-+2jndHT63cL5MdQOwDm9OT2dIe11zVpjV+0GGRXdtO1wpPxv260NfVqoEXtYAi/shanmm3W4+yLduIe55ektTw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"async": "^2.6.4",
|
|
||||||
"debug": "^3.2.7",
|
|
||||||
"mkdirp": "^0.5.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/portfinder/node_modules/debug": {
|
|
||||||
"version": "3.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
|
||||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.2",
|
"version": "8.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
|
||||||
|
|
@ -15229,13 +15102,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/secure-compare": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/select-hose": {
|
"node_modules/select-hose": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||||
|
|
@ -16768,18 +16634,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/union": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"qs": "^6.4.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unique-filename": {
|
"node_modules/unique-filename": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz",
|
||||||
|
|
@ -16877,13 +16731,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/url-join": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/url-parse": {
|
"node_modules/url-parse": {
|
||||||
"version": "1.5.10",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
|
|
@ -17495,8 +17342,8 @@
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||||
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iconv-lite": "0.6.3"
|
"iconv-lite": "0.6.3"
|
||||||
},
|
},
|
||||||
|
|
@ -17508,8 +17355,8 @@
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@
|
||||||
"@angular/compiler-cli": "^19.1.0",
|
"@angular/compiler-cli": "^19.1.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"http-server": "^14.1.1",
|
|
||||||
"jasmine-core": "~5.5.0",
|
"jasmine-core": "~5.5.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<!-- Two-column layout -->
|
<!-- Two-column layout -->
|
||||||
<div *ngIf="!trainingsDeck" class="flex flex-col md:flex-row gap-4 mx-auto max-w-6xl">
|
<div *ngIf="!trainingsDeck" class="flex flex-col md:flex-row gap-4 mx-auto max-w-5xl">
|
||||||
<!-- Left column: List of decks -->
|
<!-- Left column: List of decks -->
|
||||||
<div class="w-auto">
|
<div class="w-auto">
|
||||||
<div class="bg-white shadow rounded-lg p-4">
|
<div class="bg-white shadow rounded-lg p-4">
|
||||||
|
|
@ -47,20 +47,7 @@
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<h2 class="text-xl font-semibold">{{ activeDeck.name }}</h2>
|
<h2 class="text-xl font-semibold">{{ activeDeck.name }}</h2>
|
||||||
<!-- <span class="text-gray-600">({{ activeDeck.images.length }} images)</span> -->
|
<span class="text-gray-600">({{ activeDeck.images.length }} images)</span>
|
||||||
<div class="flex items-center mx-2">
|
|
||||||
<span class="text-sm mr-2">Shuffle</span>
|
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
|
||||||
<input type="checkbox" class="sr-only peer" [ngModel]="activeDeck.boxOrder === 'position'" (ngModelChange)="changeBoxPosition($event)" />
|
|
||||||
<div
|
|
||||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"
|
|
||||||
></div>
|
|
||||||
<span class="ml-2 text-sm font-medium">
|
|
||||||
<!-- {{ activeDeck.boxOrder === 'shuffle' ? 'Shuffle' : 'Position' }} -->
|
|
||||||
Position
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<button (click)="openDeletePopover(activeDeck.name)" class="text-red-500 hover:text-red-700" title="Delete Deck">
|
<button (click)="openDeletePopover(activeDeck.name)" class="text-red-500 hover:text-red-700" title="Delete Deck">
|
||||||
|
|
@ -75,7 +62,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <span class="text-gray-600">({{ activeDeck.images.length }} images)</span> -->
|
|
||||||
<!-- Image list -->
|
<!-- Image list -->
|
||||||
<ul class="mb-4">
|
<ul class="mb-4">
|
||||||
<li *ngFor="let image of activeDeck.images" class="flex justify-between items-center py-2 border-b last:border-b-0">
|
<li *ngFor="let image of activeDeck.images" class="flex justify-between items-center py-2 border-b last:border-b-0">
|
||||||
|
|
@ -151,7 +138,7 @@
|
||||||
<!-- <app-upload-image-modal (imageUploaded)="onImageUploaded($event)"></app-upload-image-modal> -->
|
<!-- <app-upload-image-modal (imageUploaded)="onImageUploaded($event)"></app-upload-image-modal> -->
|
||||||
<app-edit-image-modal *ngIf="imageData" [deckName]="activeDeck.name" [imageData]="imageData" (imageSaved)="onImageSaved()" (closed)="onClosed()"></app-edit-image-modal>
|
<app-edit-image-modal *ngIf="imageData" [deckName]="activeDeck.name" [imageData]="imageData" (imageSaved)="onImageSaved()" (closed)="onClosed()"></app-edit-image-modal>
|
||||||
<!-- TrainingComponent -->
|
<!-- TrainingComponent -->
|
||||||
<app-training *ngIf="trainingsDeck" [deck]="trainingsDeck" [boxOrder]="trainingsDeck.boxOrder" (close)="closeTraining()"></app-training>
|
<app-training *ngIf="trainingsDeck" [deck]="trainingsDeck" (close)="closeTraining()"></app-training>
|
||||||
<!-- MoveImageModalComponent -->
|
<!-- MoveImageModalComponent -->
|
||||||
<app-move-image-modal *ngIf="imageToMove" [image]="imageToMove.image" [sourceDeck]="imageToMove.sourceDeck" [decks]="decks" (moveCompleted)="onImageMoved()" (closed)="imageToMove = null"> </app-move-image-modal>
|
<app-move-image-modal *ngIf="imageToMove" [image]="imageToMove.image" [sourceDeck]="imageToMove.sourceDeck" [decks]="decks" (moveCompleted)="onImageMoved()" (closed)="imageToMove = null"> </app-move-image-modal>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -230,15 +230,7 @@ export class DeckListComponent implements OnInit {
|
||||||
this.trainingsDeck = null;
|
this.trainingsDeck = null;
|
||||||
this.loadDecks();
|
this.loadDecks();
|
||||||
}
|
}
|
||||||
async changeBoxPosition(val: boolean) {
|
|
||||||
this.activeDeck.boxOrder = val ? 'position' : 'shuffle';
|
|
||||||
this.deckService.updateDeck(this.activeDeck.name, { boxOrder: this.activeDeck.boxOrder }).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.loadDecks();
|
|
||||||
},
|
|
||||||
error: err => console.error('Error renaming image', err),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Method to open the create deck modal
|
// Method to open the create deck modal
|
||||||
openCreateDeckModal(): void {
|
openCreateDeckModal(): void {
|
||||||
this.createDeckModal.open();
|
this.createDeckModal.open();
|
||||||
|
|
@ -510,19 +502,25 @@ export class DeckListComponent implements OnInit {
|
||||||
//const futureDueDates = dueDates.filter(date => date && date >= now);
|
//const futureDueDates = dueDates.filter(date => date && date >= now);
|
||||||
if (dueDates.length > 0) {
|
if (dueDates.length > 0) {
|
||||||
const nextDate = dueDates.reduce((a, b) => (a < b ? a : b));
|
const nextDate = dueDates.reduce((a, b) => (a < b ? a : b));
|
||||||
return nextDate < today ? today : nextDate;
|
return nextDate;
|
||||||
}
|
}
|
||||||
return today;
|
return today;
|
||||||
}
|
}
|
||||||
getNextTrainingString(deck: Deck): string {
|
getNextTrainingString(deck: Deck): string {
|
||||||
return this.daysSinceEpochToLocalDateString(this.getNextTrainingDate(deck));
|
return this.daysSinceEpochToLocalDateString(this.getNextTrainingDate(deck));
|
||||||
}
|
}
|
||||||
|
// In deiner Component TypeScript Datei
|
||||||
|
isToday(epochDays: number): boolean {
|
||||||
|
return this.getTodayInDays() - epochDays === 0;
|
||||||
|
}
|
||||||
|
isBeforeToday(epochDays: number): boolean {
|
||||||
|
return this.getTodayInDays() - epochDays > 0;
|
||||||
|
}
|
||||||
// Methode zur Berechnung der Anzahl der zu bearbeitenden Wörter
|
// Methode zur Berechnung der Anzahl der zu bearbeitenden Wörter
|
||||||
getWordsToReview(deck: Deck): number {
|
getWordsToReview(deck: Deck): number {
|
||||||
const nextTraining = this.getNextTrainingDate(deck);
|
const nextTraining = this.getNextTrainingDate(deck);
|
||||||
const today = this.getTodayInDays();
|
const today = this.getTodayInDays();
|
||||||
return deck.images.flatMap(image => image.boxes.map(box => (box.due ? box.due : null))).filter(e => e <= nextTraining).length;
|
return deck.images.flatMap(image => image.boxes.map(box => (box.due ? box.due : null))).filter(e=>e===nextTraining).length;
|
||||||
}
|
}
|
||||||
getTodayInDays(): number {
|
getTodayInDays(): number {
|
||||||
const epoch = new Date(1970, 0, 1); // Anki uses UNIX epoch
|
const epoch = new Date(1970, 0, 1); // Anki uses UNIX epoch
|
||||||
|
|
@ -543,11 +541,4 @@ export class DeckListComponent implements OnInit {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
}).format(utcDate);
|
}).format(utcDate);
|
||||||
}
|
}
|
||||||
// In deiner Component TypeScript Datei
|
|
||||||
isToday(epochDays: number): boolean {
|
|
||||||
return this.getTodayInDays() - epochDays === 0;
|
|
||||||
}
|
|
||||||
isBeforeToday(epochDays: number): boolean {
|
|
||||||
return this.getTodayInDays() - epochDays > 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
|
|
||||||
export interface Deck {
|
export interface Deck {
|
||||||
name: string;
|
name: string;
|
||||||
boxOrder: 'shuffle' | 'position';
|
|
||||||
images: DeckImage[];
|
images: DeckImage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +62,14 @@ export class DeckService {
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
getDecks(): Observable<Deck[]> {
|
getDecks(): Observable<Deck[]> {
|
||||||
return this.http.get<any[]>(this.apiUrl);
|
return this.http.get<any[]>(this.apiUrl).pipe(
|
||||||
|
map(decks =>
|
||||||
|
decks.map(deck => ({
|
||||||
|
name: deck.name,
|
||||||
|
images: this.groupImagesByName(deck.images),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private groupImagesByName(images: any[]): DeckImage[] {
|
private groupImagesByName(images: any[]): DeckImage[] {
|
||||||
|
|
@ -97,9 +103,9 @@ export class DeckService {
|
||||||
return Object.values(imageMap);
|
return Object.values(imageMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDeck(deckname: string): Observable<Deck> {
|
getDeck(deckname: string): Observable<Deck> {
|
||||||
// return this.http.get<Deck>(`${this.apiUrl}/${deckname}/images`);
|
return this.http.get<Deck>(`${this.apiUrl}/${deckname}/images`);
|
||||||
// }
|
}
|
||||||
|
|
||||||
createDeck(deckname: string): Observable<any> {
|
createDeck(deckname: string): Observable<any> {
|
||||||
return this.http.post(this.apiUrl, { deckname });
|
return this.http.post(this.apiUrl, { deckname });
|
||||||
|
|
@ -111,9 +117,6 @@ export class DeckService {
|
||||||
renameDeck(oldDeckName: string, newDeckName: string): Observable<any> {
|
renameDeck(oldDeckName: string, newDeckName: string): Observable<any> {
|
||||||
return this.http.put(`${this.apiUrl}/${encodeURIComponent(oldDeckName)}/rename`, { newDeckName });
|
return this.http.put(`${this.apiUrl}/${encodeURIComponent(oldDeckName)}/rename`, { newDeckName });
|
||||||
}
|
}
|
||||||
updateDeck(oldDeckName: string, updateData: { newName?: string; boxOrder?: 'shuffle' | 'position' }): Observable<any> {
|
|
||||||
return this.http.put(`${this.apiUrl}/${encodeURIComponent(oldDeckName)}/update`, updateData);
|
|
||||||
}
|
|
||||||
renameImage(bildid: string, newImageName: string): Observable<any> {
|
renameImage(bildid: string, newImageName: string): Observable<any> {
|
||||||
return this.http.put(`${this.apiUrl}/image/${encodeURIComponent(bildid)}/rename`, { newImageName });
|
return this.http.put(`${this.apiUrl}/image/${encodeURIComponent(bildid)}/rename`, { newImageName });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="mt-10 mx-auto max-w-5xl">
|
<div class="mt-10 mx-auto max-w-5xl">
|
||||||
<h2 class="text-2xl font-bold mb-4">Training: {{ deck.name }}</h2>
|
<h2 class="text-2xl font-bold mb-4">Training: {{ deck.name }}</h2>
|
||||||
<div class="rounded-lg p-6 flex flex-col items-center">
|
<div class="rounded-lg p-6 flex flex-col items-center">
|
||||||
<canvas #canvas class="mb-4 border max-h-[70vh]"></canvas>
|
<canvas #canvas class="mb-4 border max-h-[50vh]"></canvas>
|
||||||
|
|
||||||
<div class="flex space-x-4 mb-4">
|
<div class="flex space-x-4 mb-4">
|
||||||
<!-- Show Button -->
|
<!-- Show Button -->
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ const EASY_INTERVAL = 4 * 1440; // 4 days in minutes
|
||||||
})
|
})
|
||||||
export class TrainingComponent implements OnInit {
|
export class TrainingComponent implements OnInit {
|
||||||
@Input() deck!: Deck;
|
@Input() deck!: Deck;
|
||||||
@Input() boxOrder: 'shuffle' | 'position' = 'shuffle'; // Standardmäßig shuffle
|
|
||||||
@Output() close = new EventEmitter<void>();
|
@Output() close = new EventEmitter<void>();
|
||||||
|
|
||||||
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
@ -93,13 +92,8 @@ export class TrainingComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order the boxes based on the input parameter
|
// Shuffle the boxes randomly
|
||||||
if (this.boxOrder === 'position') {
|
this.boxesToReview = this.shuffleArray(this.boxesToReview);
|
||||||
this.boxesToReview = this.sortBoxesByPosition(this.boxesToReview);
|
|
||||||
} else {
|
|
||||||
// Default: shuffle
|
|
||||||
this.boxesToReview = this.shuffleArray(this.boxesToReview);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the array to track revealed boxes
|
// Initialize the array to track revealed boxes
|
||||||
this.boxRevealed = new Array(this.boxesToReview.length).fill(false);
|
this.boxRevealed = new Array(this.boxesToReview.length).fill(false);
|
||||||
|
|
@ -171,43 +165,7 @@ export class TrainingComponent implements OnInit {
|
||||||
this.close.emit();
|
this.close.emit();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Sorts boxes by their position (left to right, top to bottom)
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Sorts boxes by their position (left to right, top to bottom) with better row detection
|
|
||||||
*/
|
|
||||||
sortBoxesByPosition(boxes: Box[]): Box[] {
|
|
||||||
// First create a copy of the array
|
|
||||||
const boxesCopy = [...boxes];
|
|
||||||
|
|
||||||
// Determine the average box height to use for row grouping tolerance
|
|
||||||
const avgHeight = boxesCopy.reduce((sum, box) => sum + (box.y2 - box.y1), 0) / boxesCopy.length;
|
|
||||||
const rowTolerance = avgHeight * 0.4; // 40% of average box height as tolerance
|
|
||||||
|
|
||||||
// Group boxes into rows
|
|
||||||
const rows: Box[][] = [];
|
|
||||||
|
|
||||||
boxesCopy.forEach(box => {
|
|
||||||
// Find an existing row within tolerance
|
|
||||||
const row = rows.find(r => Math.abs(r[0].y1 - box.y1) < rowTolerance || Math.abs(r[0].y2 - box.y2) < rowTolerance);
|
|
||||||
|
|
||||||
if (row) {
|
|
||||||
row.push(box);
|
|
||||||
} else {
|
|
||||||
rows.push([box]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort each row by x1 (left to right)
|
|
||||||
rows.forEach(row => row.sort((a, b) => a.x1 - b.x1));
|
|
||||||
|
|
||||||
// Sort rows by y1 (top to bottom)
|
|
||||||
rows.sort((a, b) => a[0].y1 - b[0].y1);
|
|
||||||
|
|
||||||
// Flatten the rows into a single array
|
|
||||||
return rows.flat();
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Shuffles an array randomly.
|
* Shuffles an array randomly.
|
||||||
* @param array The array to shuffle
|
* @param array The array to shuffle
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue