Skip to content

Commit 2be666b

Browse files
authored
Merge pull request #31 from chytanka/feat/save-files-history
Feat/save files history
2 parents cff6043 + 08e42e6 commit 2be666b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1044
-165
lines changed

.github/workflows/node.js.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020

2121
strategy:
2222
matrix:
23-
node-version: [18.x]
23+
node-version: [22.x]
2424
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
2525

2626
steps:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "chytanka",
3-
"version": "0.14.30",
3+
"version": "0.14.31",
44
"scripts": {
55
"ng": "ng",
66
"start": "ng serve",

src/app/app.component.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { Component, HostListener, PLATFORM_ID, WritableSignal, inject, signal }
22
import { LangService } from './shared/data-access/lang.service';
33
import { ActivatedRoute } from '@angular/router';
44
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
5+
import { environment } from '../environments/environment';
56

67
const SCALE_GAP = 128;
78

89
@Component({
9-
selector: 'app-root',
10+
selector: 'chtnk-root',
1011
template: `<div><router-outlet></router-outlet></div><div><router-outlet name="right"></router-outlet></div>`,
1112
styles: [`
1213
// :host {
@@ -23,7 +24,7 @@ export class AppComponent {
2324
this.lang.updateManifest()
2425
this.lang.updateTranslate()
2526

26-
if (isPlatformBrowser(this.platformId) && window.console) {
27+
if (isPlatformBrowser(this.platformId) && window.console && environment.prod) {
2728
const msg = `What are you looking for here? The plot twist is in the next volume!`
2829
console.log(`%c${msg}`, "background-color: #166496; color: #ffd60a; font-size: 4rem; font-family: monospace; padding: 8px 16px");
2930
}

src/app/app.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { LOCALE_ID, NgModule, isDevMode, provideZoneChangeDetection } from '@angular/core';
2-
import { BrowserModule, provideClientHydration } from '@angular/platform-browser';
2+
import { BrowserModule, provideClientHydration, withEventReplay } from '@angular/platform-browser';
33

44
import { AppRoutingModule } from './app-routing.module';
55
import { AppComponent } from './app.component';
@@ -30,7 +30,7 @@ registerLocaleData(localeUk)
3030
SharedModule],
3131
providers: [
3232
provideZoneChangeDetection({ eventCoalescing: true }),
33-
provideClientHydration(),
33+
provideClientHydration(withEventReplay()),
3434
provideHttpClient(withFetch())
3535
]
3636
})

src/app/file/data-access/file-hash.service.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,41 @@ import * as CryptoJS from 'crypto-js';
55
providedIn: 'root'
66
})
77
export class FileHashService {
8-
public getMd5Hash(arrayBuffer: ArrayBuffer) {
9-
const wordArray = this.arrayBufferToWordArray(arrayBuffer);
10-
const md5Hash = CryptoJS.MD5(wordArray).toString();
11-
return md5Hash;
12-
}
8+
// getSHA256(arrayBuffer: ArrayBuffer): string {
9+
// const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(arrayBuffer) as any);
10+
// return CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.Hex);
11+
// }
1312

14-
private arrayBufferToWordArray(arrayBuffer: ArrayBuffer): CryptoJS.lib.WordArray {
15-
const uint8Array = new Uint8Array(arrayBuffer);
16-
const words: any[] = [];
17-
for (let i = 0; i < uint8Array.length; i++) {
18-
words[i >>> 2] |= uint8Array[i] << (24 - (i % 4) * 8);
13+
async sha256(file: File): Promise<string> {
14+
const sha256 = CryptoJS.algo.SHA256.create();
15+
const sliceSize = 50_000_000; // 50 MiB
16+
let start = 0;
17+
18+
while (start < file.size) {
19+
const slice: Uint8Array = await this.readSlice(file, start, sliceSize);
20+
const wordArray = CryptoJS.lib.WordArray.create(slice);
21+
sha256.update(wordArray);
22+
start += sliceSize;
1923
}
20-
return CryptoJS.lib.WordArray.create(words, uint8Array.length);
24+
25+
return sha256.finalize().toString();
26+
}
27+
28+
private async readSlice(file: File, start: number, size: number): Promise<Uint8Array> {
29+
return new Promise<Uint8Array>((resolve, reject) => {
30+
const fileReader = new FileReader();
31+
const slice = file.slice(start, start + size);
32+
33+
fileReader.onload = () => {
34+
if (fileReader.result) {
35+
resolve(new Uint8Array(fileReader.result as ArrayBuffer));
36+
} else {
37+
reject(new Error("FileReader returned no result."));
38+
}
39+
};
40+
fileReader.onerror = () => reject(fileReader.error || new Error("FileReader failed."));
41+
fileReader.readAsArrayBuffer(slice);
42+
});
2143
}
44+
2245
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { isPlatformBrowser } from '@angular/common';
2+
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
3+
import Dexie from 'dexie';
4+
5+
const HISTORY_DB_NAME: string = `ChytankaHistoryDB`;
6+
const HISTORY_TABLE_NAME: string = `filehistory`;
7+
8+
@Injectable({
9+
providedIn: 'root'
10+
})
11+
export class FileHistoryService {
12+
private db!: Dexie;
13+
14+
platformId = inject(PLATFORM_ID)
15+
16+
constructor() {
17+
this.createDatabase();
18+
}
19+
20+
private createDatabase() {
21+
this.db = new Dexie(HISTORY_DB_NAME);
22+
this.db.version(1).stores({
23+
filehistory: '++id,sha256,pages,size,title,format,page,cover,arrayBuffer,created,updated'
24+
});
25+
}
26+
27+
async addHistory(fileHistory: any) {
28+
if (!isPlatformBrowser(this.platformId)) return;
29+
30+
const { sha256, arrayBuffer, pages, page, cover, size, title, format } = fileHistory
31+
32+
// await this.db.table(HISTORY_TABLE_NAME).add({ site, post_id, title, cover });
33+
const existingEntry = await this.db.table(HISTORY_TABLE_NAME).where({ sha256 }).first();
34+
35+
if (existingEntry) {
36+
// Entry already exists, update the 'updated' field
37+
const now = new Date().toISOString();
38+
await this.db.table(HISTORY_TABLE_NAME).update(existingEntry.id, { arrayBuffer, updated: now });
39+
} else {
40+
// Entry doesn't exist, add a new one
41+
const now = new Date().toISOString();
42+
await this.db.table(HISTORY_TABLE_NAME).add({
43+
created: now,
44+
updated: now,
45+
sha256,
46+
arrayBuffer,
47+
pages, page, cover, size, title, format
48+
});
49+
}
50+
}
51+
52+
async getAllHistory() {
53+
return await this.db.table(HISTORY_TABLE_NAME).orderBy('updated').reverse().toArray();
54+
}
55+
56+
async getItemBySha256(sha256: string) {
57+
return await this.db.table(HISTORY_TABLE_NAME).where('sha256').equals(sha256).first();
58+
}
59+
60+
61+
async getAllHistoryWithoutBufferArray() {
62+
const records = await this.db.table(HISTORY_TABLE_NAME).orderBy('updated').reverse().toArray();
63+
64+
return records.map(({ arrayBuffer, ...rest }) => rest);
65+
}
66+
67+
async getTotalSizeAndCount(): Promise<{ size: number; count: number }> {
68+
let size = 0;
69+
let count = 0;
70+
71+
await this.db.table(HISTORY_TABLE_NAME).each(item => {
72+
size += item.size;
73+
count++;
74+
});
75+
76+
return { size, count };
77+
}
78+
79+
async clearHistory() {
80+
await this.db.table(HISTORY_TABLE_NAME).clear();
81+
}
82+
83+
async deleteHistoryItem(itemId: number) {
84+
await this.db.table(HISTORY_TABLE_NAME).delete(itemId);
85+
}
86+
87+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { isPlatformServer } from '@angular/common';
2+
import { inject, Injectable, PLATFORM_ID, signal, WritableSignal } from '@angular/core';
3+
4+
const SAVE_FILE_HISTORY_NAME = "saveFileToHistory"
5+
const COPY_FILE_HISTORY_NAME = "copyFileToHistory"
6+
const RETENTION_TIME_NAME = "retentionTime"
7+
const STORAGE_LIMIT_NAME = "storageLimit"
8+
9+
const DEFAULT_RETENTION_TIME = 30; // days
10+
const DEFAULT_STORAGE_LIMIT = 1024; //Mb
11+
12+
@Injectable({
13+
providedIn: 'root'
14+
})
15+
export class FileSettingsService {
16+
platformId = inject(PLATFORM_ID)
17+
18+
readonly saveFileToHistory: WritableSignal<boolean> = signal(false);
19+
readonly copyFileToHistory: WritableSignal<boolean> = signal(false);
20+
readonly retentionTime: WritableSignal<number> = signal(DEFAULT_RETENTION_TIME)
21+
readonly storageLimit: WritableSignal<number> = signal(DEFAULT_STORAGE_LIMIT)
22+
23+
constructor() {
24+
this.init()
25+
}
26+
27+
init() {
28+
if (isPlatformServer(this.platformId)) return;
29+
30+
const n = Boolean(localStorage.getItem(SAVE_FILE_HISTORY_NAME) == 'true');
31+
this.setSaveFileToHistory(n);
32+
33+
const sf = Boolean(localStorage.getItem(COPY_FILE_HISTORY_NAME) == 'true');
34+
this.setCopyFileToHistory(sf);
35+
36+
const rt = Number(localStorage.getItem(RETENTION_TIME_NAME) ?? DEFAULT_RETENTION_TIME);
37+
this.setRetentionTime(rt)
38+
39+
const sl = Number(localStorage.getItem(STORAGE_LIMIT_NAME) ?? DEFAULT_STORAGE_LIMIT);
40+
this.setStorageLimit(sl)
41+
}
42+
43+
setSaveFileToHistory(n: boolean) {
44+
if (isPlatformServer(this.platformId)) return;
45+
46+
this.saveFileToHistory.set(n);
47+
localStorage.setItem(SAVE_FILE_HISTORY_NAME, n.toString())
48+
}
49+
50+
setCopyFileToHistory(n: boolean) {
51+
if (isPlatformServer(this.platformId)) return;
52+
53+
this.copyFileToHistory.set(n);
54+
localStorage.setItem(COPY_FILE_HISTORY_NAME, n.toString())
55+
}
56+
57+
setRetentionTime(days: number) {
58+
if (isPlatformServer(this.platformId)) return;
59+
60+
this.retentionTime.set(days);
61+
localStorage.setItem(RETENTION_TIME_NAME, days.toString())
62+
}
63+
64+
setStorageLimit(megabytes: number) {
65+
if (isPlatformServer(this.platformId)) return;
66+
67+
this.storageLimit.set(megabytes);
68+
localStorage.setItem(STORAGE_LIMIT_NAME, megabytes.toString())
69+
}
70+
}

src/app/file/data-access/zip.worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ addEventListener('message', ({ data }) => {
1111
.then(async zip => {
1212
const filesName: string[] = Object.keys(zip.files);
1313

14-
console.dir(zip.files)
14+
// console.dir(zip.files)
1515

1616
const comicInfoFile = getComicInfoFile(filesName)
1717

src/app/file/file-routing.module.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { NgModule } from '@angular/core';
22
import { RouterModule, Routes } from '@angular/router';
33

4+
const zip = () => import('./zip/zip.component').then(mod => mod.ZipComponent);
5+
const pdf = () => import('./pdf/pdf.component').then(mod => mod.PdfComponent);
6+
const mobi = () => import('./mobi/mobi.component').then(mod => mod.MobiComponent);
7+
48
const routes: Routes = [
5-
{
6-
path: 'zip',
7-
loadComponent: () => import('./zip/zip.component').then(mod => mod.ZipComponent)
8-
},
9-
{
10-
path: 'pdf',
11-
loadComponent: () => import('./pdf/pdf.component').then(mod => mod.PdfComponent)
12-
},
13-
{
14-
path: 'mobi',
15-
loadComponent: () => import('./mobi/mobi.component').then(mod => mod.MobiComponent)
16-
}
9+
{ path: 'zip/:sha256', loadComponent: zip },
10+
{ path: 'zip', loadComponent: zip },
11+
12+
{ path: 'pdf/:sha256', loadComponent: pdf },
13+
{ path: 'pdf', loadComponent: pdf },
14+
15+
{ path: 'mobi/:sha256', loadComponent: mobi },
16+
{ path: 'mobi', loadComponent: mobi }
1717
];
1818

1919
@NgModule({

src/app/file/zip/zip.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@if(episode && episode.images && episode.images.length > 0){
2-
<app-viewer [episode]="episode" />
2+
<!-- <chtnk-viewer [episode]="episode" class="right-to-left"/> -->
3+
<app-viewer [episode]="episode" (pagechange)="onPageChange($event)" />
34
} @else {
45
<loading />
56
}

0 commit comments

Comments
 (0)