@@ -4,9 +4,11 @@ import 'package:path/path.dart' as p;
44import 'package:path_provider/path_provider.dart' ;
55
66/// 字幕文件数据库记录
7+ ///
8+ /// 数据库只存储相对路径(相对于字幕库根目录),绝对路径在运行时动态拼接。
9+ /// 这样即使 iOS 重装后沙盒 UUID 变化,或其他平台迁移下载目录,数据库无需更新。
710class SubtitleFileRecord {
811 final String fileName;
9- final String filePath;
1012 final String relativePath;
1113 final String category;
1214 final int ? workId;
@@ -16,7 +18,6 @@ class SubtitleFileRecord {
1618
1719 SubtitleFileRecord ({
1820 required this .fileName,
19- required this .filePath,
2021 required this .relativePath,
2122 required this .category,
2223 this .workId,
@@ -25,9 +26,13 @@ class SubtitleFileRecord {
2526 this .normalizedName,
2627 });
2728
29+ /// 根据字幕库根目录拼接出绝对路径
30+ String absolutePath (String libraryRootPath) {
31+ return p.joinAll ([libraryRootPath, ...relativePath.split ('/' )]);
32+ }
33+
2834 Map <String , dynamic > toMap () => {
2935 'file_name' : fileName,
30- 'file_path' : filePath,
3136 'relative_path' : relativePath,
3237 'category' : category,
3338 'work_id' : workId,
@@ -39,7 +44,6 @@ class SubtitleFileRecord {
3944 factory SubtitleFileRecord .fromMap (Map <String , dynamic > map) {
4045 return SubtitleFileRecord (
4146 fileName: map['file_name' ] as String ,
42- filePath: map['file_path' ] as String ,
4347 relativePath: map['relative_path' ] as String ,
4448 category: map['category' ] as String ,
4549 workId: map['work_id' ] as int ? ,
@@ -76,8 +80,9 @@ class SubtitleDatabase {
7680
7781 return await openDatabase (
7882 path,
79- version: 1 ,
83+ version: 2 ,
8084 onCreate: _createDB,
85+ onUpgrade: _upgradeDB,
8186 );
8287 }
8388
@@ -86,8 +91,7 @@ class SubtitleDatabase {
8691 CREATE TABLE subtitle_files (
8792 id INTEGER PRIMARY KEY AUTOINCREMENT,
8893 file_name TEXT NOT NULL,
89- file_path TEXT NOT NULL UNIQUE,
90- relative_path TEXT NOT NULL,
94+ relative_path TEXT NOT NULL UNIQUE,
9195 category TEXT NOT NULL,
9296 work_id INTEGER,
9397 file_size INTEGER DEFAULT 0,
@@ -101,8 +105,6 @@ class SubtitleDatabase {
101105 'CREATE INDEX idx_files_work_id ON subtitle_files(work_id)' );
102106 await db.execute (
103107 'CREATE INDEX idx_files_category ON subtitle_files(category)' );
104- await db.execute (
105- 'CREATE INDEX idx_files_file_path ON subtitle_files(file_path)' );
106108
107109 await db.execute ('''
108110 CREATE TABLE library_meta (
@@ -112,6 +114,41 @@ class SubtitleDatabase {
112114 ''' );
113115 }
114116
117+ Future <void > _upgradeDB (Database db, int oldVersion, int newVersion) async {
118+ if (oldVersion < 2 ) {
119+ // v2: 移除 file_path 列,改用 relative_path 作为唯一键
120+ // SQLite 不支持直接 DROP COLUMN,需要重建表
121+ await db.execute (
122+ 'ALTER TABLE subtitle_files RENAME TO subtitle_files_old' );
123+ await db.execute ('''
124+ CREATE TABLE subtitle_files (
125+ id INTEGER PRIMARY KEY AUTOINCREMENT,
126+ file_name TEXT NOT NULL,
127+ relative_path TEXT NOT NULL UNIQUE,
128+ category TEXT NOT NULL,
129+ work_id INTEGER,
130+ file_size INTEGER DEFAULT 0,
131+ modified_at TEXT,
132+ normalized_name TEXT,
133+ created_at TEXT DEFAULT (datetime('now'))
134+ )
135+ ''' );
136+ await db.execute ('''
137+ INSERT OR REPLACE INTO subtitle_files
138+ (file_name, relative_path, category, work_id, file_size,
139+ modified_at, normalized_name, created_at)
140+ SELECT file_name, relative_path, category, work_id, file_size,
141+ modified_at, normalized_name, created_at
142+ FROM subtitle_files_old
143+ ''' );
144+ await db.execute ('DROP TABLE subtitle_files_old' );
145+ await db.execute (
146+ 'CREATE INDEX idx_files_work_id ON subtitle_files(work_id)' );
147+ await db.execute (
148+ 'CREATE INDEX idx_files_category ON subtitle_files(category)' );
149+ }
150+ }
151+
115152 // ==================== CRUD ====================
116153
117154 /// 插入单条记录
@@ -133,75 +170,70 @@ class SubtitleDatabase {
133170 await batch.commit (noResult: true );
134171 }
135172
136- /// 按文件路径删除
137- Future <int > deleteByPath (String filePath ) async {
173+ /// 按相对路径删除
174+ Future <int > deleteByRelativePath (String relativePath ) async {
138175 final db = await database;
139176 return await db.delete ('subtitle_files' ,
140- where: 'file_path = ?' , whereArgs: [filePath ]);
177+ where: 'relative_path = ?' , whereArgs: [relativePath ]);
141178 }
142179
143- /// 按路径前缀删除 (用于删除目录下所有文件)
144- Future <int > deleteByPathPrefix (String directoryPath ) async {
180+ /// 按相对路径前缀删除 (用于删除目录下所有文件)
181+ Future <int > deleteByRelativePathPrefix (String relativePrefix ) async {
145182 final db = await database;
146183 // 确保路径以 / 结尾,避免误删同前缀的其他目录
147184 final prefix =
148- directoryPath .endsWith ('/' ) ? directoryPath : '$directoryPath /' ;
185+ relativePrefix .endsWith ('/' ) ? relativePrefix : '$relativePrefix /' ;
149186 return await db.delete ('subtitle_files' ,
150- where: 'file_path LIKE ?' , whereArgs: ['$prefix %' ]);
187+ where: 'relative_path LIKE ?' , whereArgs: ['$prefix %' ]);
151188 }
152189
153190 /// 更新单个文件的路径(重命名)
154- Future <void > updateFilePath (
155- String oldPath,
156- String newFilePath,
191+ Future <void > updateRelativePath (
192+ String oldRelativePath,
157193 String newRelativePath,
158194 String newFileName,
159195 ) async {
160196 final db = await database;
197+ final newCategory = _extractCategoryFromRelativePath (newRelativePath);
198+ final newWorkId = _extractWorkIdFromRelativePath (newRelativePath);
161199 await db.update (
162200 'subtitle_files' ,
163201 {
164- 'file_path' : newFilePath,
165202 'relative_path' : newRelativePath,
166203 'file_name' : newFileName,
204+ 'category' : newCategory,
205+ 'work_id' : newWorkId,
167206 },
168- where: 'file_path = ?' ,
169- whereArgs: [oldPath ],
207+ where: 'relative_path = ?' ,
208+ whereArgs: [oldRelativePath ],
170209 );
171210 }
172211
173212 /// 批量更新目录路径(目录重命名/移动)
174- Future <void > updateDirectoryPaths (
175- String oldDirPath,
176- String newDirPath,
177- String libraryRootPath,
213+ Future <void > updateDirectoryRelativePaths (
214+ String oldRelativePrefix,
215+ String newRelativePrefix,
178216 ) async {
179217 final db = await database;
180218 final oldPrefix =
181- oldDirPath .endsWith ('/' ) ? oldDirPath : '$oldDirPath /' ;
219+ oldRelativePrefix .endsWith ('/' ) ? oldRelativePrefix : '$oldRelativePrefix /' ;
182220 final files = await db.query ('subtitle_files' ,
183- where: 'file_path LIKE ?' , whereArgs: ['$oldPrefix %' ]);
221+ where: 'relative_path LIKE ?' , whereArgs: ['$oldPrefix %' ]);
184222
185223 if (files.isEmpty) return ;
186224
187225 final batch = db.batch ();
188226 for (final file in files) {
189- final oldFilePath = file['file_path' ] as String ;
190- final newFilePath = oldFilePath.replaceFirst (oldDirPath, newDirPath);
191- var newRelativePath =
192- newFilePath.substring (libraryRootPath.length + 1 );
193- // 统一用 / 存储相对路径
194- if (Platform .isWindows) {
195- newRelativePath = newRelativePath.replaceAll ('\\ ' , '/' );
196- }
227+ final oldRelativePath = file['relative_path' ] as String ;
228+ final newRelativePath =
229+ oldRelativePath.replaceFirst (oldRelativePrefix, newRelativePrefix);
197230 final parts = newRelativePath.split ('/' );
198231 final newCategory = parts.isNotEmpty ? parts.first : '' ;
199232 final newWorkId = _extractWorkIdFromRelativePath (newRelativePath);
200233
201234 batch.update (
202235 'subtitle_files' ,
203236 {
204- 'file_path' : newFilePath,
205237 'relative_path' : newRelativePath,
206238 'category' : newCategory,
207239 'work_id' : newWorkId,
@@ -332,6 +364,15 @@ class SubtitleDatabase {
332364
333365 static final _workIdRegex = RegExp (r'[RrBbVv][Jj]0*(\d+)' );
334366
367+ /// 从相对路径提取分类(第一级目录)
368+ static String _extractCategoryFromRelativePath (String relativePath) {
369+ final firstSlash = relativePath.indexOf ('/' );
370+ if (firstSlash > 0 ) {
371+ return relativePath.substring (0 , firstSlash);
372+ }
373+ return '' ;
374+ }
375+
335376 /// 从相对路径提取 workId
336377 static int ? _extractWorkIdFromRelativePath (String relativePath) {
337378 // 相对路径格式: "已解析/RJ1003058/track.vtt"
0 commit comments