Rename to Claude Session Analysis and prepare for Mac App Store
- Rename package, bundle ID, and all display strings from "Claude Session Viewer" to "Claude Session Analysis" - Bundle ID: com.svrnty.claudeSessionAnalysis - Add App Store category (developer-tools) to Info.plist - Add PrivacyInfo.xcprivacy privacy manifest (required by Apple) - Bump deployment target from macOS 10.15 to 13.0 - Fix App Sandbox: resolve real home directory so ~/.claude/projects is accessible in sandboxed release builds - Make project name parsing dynamic (no hardcoded username) - Auto-detect Claude Code data directory at ~/.claude/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
364877d376
commit
aa484f6409
@ -5,18 +5,18 @@ import 'theme/app_theme.dart';
|
|||||||
import 'widgets/navigation/app_shell.dart';
|
import 'widgets/navigation/app_shell.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const ClaudeSessionViewerApp());
|
runApp(const ClaudeSessionAnalysisApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
class ClaudeSessionViewerApp extends StatelessWidget {
|
class ClaudeSessionAnalysisApp extends StatelessWidget {
|
||||||
const ClaudeSessionViewerApp({super.key});
|
const ClaudeSessionAnalysisApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (_) => SessionProvider(),
|
create: (_) => SessionProvider(),
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'Claude Session Viewer',
|
title: 'Claude Session Analysis',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: AppTheme.dark,
|
theme: AppTheme.dark,
|
||||||
home: const Scaffold(
|
home: const Scaffold(
|
||||||
|
|||||||
@ -107,58 +107,58 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
_scanProjects();
|
_scanProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the real user home directory, even inside App Sandbox.
|
||||||
|
/// In sandbox, HOME points to ~/Library/Containers/<bundleid>/Data.
|
||||||
|
String? _getRealHome() {
|
||||||
|
final home = Platform.environment['HOME'];
|
||||||
|
if (home == null) return null;
|
||||||
|
final match = RegExp(r'^(/Users/[^/]+)/Library/Containers/').firstMatch(home);
|
||||||
|
if (match != null) return match.group(1);
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts the encoded dir name to a human-readable project name.
|
/// Converts the encoded dir name to a human-readable project name.
|
||||||
/// "-Users-mathias-Documents-workspaces-svrnty-talos-rpi5" → "svrnty / talos-rpi5"
|
/// "-Users-mathias-Documents-workspaces-svrnty-talos-rpi5" → "svrnty / talos-rpi5"
|
||||||
/// "-Users-mathias" → "~ (home)"
|
/// "-Users-mathias" → "~ (home)"
|
||||||
/// "-Applications-Auto-Claude-app-Contents-Resources-backend" → "Auto-Claude / backend"
|
|
||||||
String _parsePrettyName(String dirName) {
|
String _parsePrettyName(String dirName) {
|
||||||
// Reconstruct the original path: leading - is /, inner - are /
|
final home = _getRealHome() ?? '';
|
||||||
// But careful: "a-gent-maf-debug" is a single folder name with dashes.
|
final username = home.split('/').last;
|
||||||
// The trick: the encoded path uses - as separator for EVERY path component.
|
|
||||||
// We know the common prefixes so we can strip them.
|
|
||||||
String path = dirName;
|
|
||||||
|
|
||||||
// Strip known prefixes to get to the interesting part
|
// Try to reconstruct the actual filesystem path
|
||||||
|
final reconstructed = _reconstructPath(dirName, home);
|
||||||
|
if (reconstructed != null) {
|
||||||
|
final segs = reconstructed.split('/').where((s) => s.isNotEmpty).toList();
|
||||||
|
// Remove common uninteresting segments
|
||||||
|
final skip = {'Users', username, 'Documents', 'workspaces', 'Workspaces',
|
||||||
|
'Applications', 'Contents', 'Resources', 'Volumes'};
|
||||||
|
final meaningful = segs.where((s) => !skip.contains(s)).toList();
|
||||||
|
if (meaningful.isEmpty) return '~ (home)';
|
||||||
|
if (meaningful.length >= 2) {
|
||||||
|
return '${meaningful[meaningful.length - 2]} / ${meaningful.last}';
|
||||||
|
}
|
||||||
|
return meaningful.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: build dynamic prefixes from the detected username
|
||||||
final prefixes = [
|
final prefixes = [
|
||||||
'-Users-mathias-Documents-workspaces-',
|
'-Users-$username-Documents-workspaces-',
|
||||||
'-Users-mathias-Documents-',
|
'-Users-$username-Documents-',
|
||||||
'-Users-mathias-',
|
'-Users-$username-Workspaces-',
|
||||||
|
'-Users-$username-',
|
||||||
|
'-Volumes-Workspaces-',
|
||||||
|
'-Volumes-',
|
||||||
'-Applications-',
|
'-Applications-',
|
||||||
];
|
];
|
||||||
|
|
||||||
String prefix = '';
|
String path = dirName;
|
||||||
for (final p in prefixes) {
|
for (final p in prefixes) {
|
||||||
if (path.startsWith(p)) {
|
if (path.startsWith(p)) {
|
||||||
prefix = p;
|
|
||||||
path = path.substring(p.length);
|
path = path.substring(p.length);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.isEmpty) {
|
if (path.isEmpty) return '~ (home)';
|
||||||
if (dirName == '-Users-mathias') return '~ (home)';
|
|
||||||
return dirName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now `path` is something like "svrnty-talos-rpi5--out-fondation"
|
|
||||||
// Double dashes (--) were actual dashes in folder names? No — they represent
|
|
||||||
// a subfolder that itself has a dash. We need to figure out the actual
|
|
||||||
// filesystem path. Let's just check if the reconstructed path exists.
|
|
||||||
final home = Platform.environment['HOME'] ?? '';
|
|
||||||
final reconstructed = _reconstructPath(dirName, home);
|
|
||||||
if (reconstructed != null) {
|
|
||||||
// Get last 2 meaningful segments
|
|
||||||
final segs = reconstructed.split('/').where((s) => s.isNotEmpty).toList();
|
|
||||||
// Remove common uninteresting segments
|
|
||||||
final skip = {'Users', 'mathias', 'Documents', 'workspaces', 'Applications', 'Contents', 'Resources'};
|
|
||||||
final meaningful = segs.where((s) => !skip.contains(s)).toList();
|
|
||||||
if (meaningful.length >= 2) {
|
|
||||||
return '${meaningful[meaningful.length - 2]} / ${meaningful.last}';
|
|
||||||
}
|
|
||||||
if (meaningful.isNotEmpty) return meaningful.last;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: just clean up the raw name
|
|
||||||
return path.replaceAll('--', ' / ').replaceAll('-', ' ');
|
return path.replaceAll('--', ' / ').replaceAll('-', ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,18 +200,48 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
return result; // return anyway as best guess
|
return result; // return anyway as best guess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Locates the Claude Code data directory.
|
||||||
|
/// Claude Code always stores data at ~/.claude/ regardless of where
|
||||||
|
/// the binary is installed (npm global, homebrew, etc).
|
||||||
|
Future<Directory?> _findClaudeDataDir() async {
|
||||||
|
final home = _getRealHome();
|
||||||
|
debugPrint('[Claude] HOME env: ${Platform.environment['HOME']}');
|
||||||
|
debugPrint('[Claude] Real home resolved to: $home');
|
||||||
|
|
||||||
|
if (home == null) {
|
||||||
|
debugPrint('[Claude] ERROR: Could not determine home directory');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final claudeDir = Directory('$home/.claude');
|
||||||
|
final exists = await claudeDir.exists();
|
||||||
|
debugPrint('[Claude] Checking ${claudeDir.path} -> exists: $exists');
|
||||||
|
|
||||||
|
if (exists) return claudeDir;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _scanProjects() async {
|
Future<void> _scanProjects() async {
|
||||||
final home = Platform.environment['HOME'];
|
final claudeData = await _findClaudeDataDir();
|
||||||
if (home == null) return;
|
if (claudeData == null) {
|
||||||
final claudeDir = Directory('$home/.claude/projects');
|
debugPrint('[Claude] No Claude Code data directory found');
|
||||||
if (!await claudeDir.exists()) {
|
|
||||||
setState(() => _scanning = false);
|
setState(() => _scanning = false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final projectsDir = Directory('${claudeData.path}/projects');
|
||||||
|
final projExists = await projectsDir.exists();
|
||||||
|
debugPrint('[Claude] Projects dir: ${projectsDir.path} -> exists: $projExists');
|
||||||
|
if (!projExists) {
|
||||||
|
setState(() => _scanning = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final home = _getRealHome() ?? '';
|
||||||
final projects = <_Project>[];
|
final projects = <_Project>[];
|
||||||
|
|
||||||
await for (final projectDir in claudeDir.list()) {
|
await for (final projectDir in projectsDir.list()) {
|
||||||
if (projectDir is! Directory) continue;
|
if (projectDir is! Directory) continue;
|
||||||
final dirName = projectDir.path.split('/').last;
|
final dirName = projectDir.path.split('/').last;
|
||||||
|
|
||||||
@ -220,9 +250,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
// Scan .jsonl files in the project dir
|
// Scan .jsonl files in the project dir
|
||||||
try {
|
try {
|
||||||
await for (final entity in projectDir.list()) {
|
await for (final entity in projectDir.list()) {
|
||||||
if (entity is File &&
|
if (entity is File && entity.path.endsWith('.jsonl')) {
|
||||||
entity.path.endsWith('.jsonl') &&
|
|
||||||
!entity.path.endsWith('sessions-index.json')) {
|
|
||||||
try {
|
try {
|
||||||
final stat = entity.statSync();
|
final stat = entity.statSync();
|
||||||
final fileName = entity.path.split('/').last;
|
final fileName = entity.path.split('/').last;
|
||||||
@ -236,26 +264,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// Also scan sessions/ subdirectory
|
|
||||||
final sessionsDir = Directory('${projectDir.path}/sessions');
|
|
||||||
if (await sessionsDir.exists()) {
|
|
||||||
try {
|
|
||||||
await for (final entity in sessionsDir.list()) {
|
|
||||||
if (entity is File && entity.path.endsWith('.jsonl')) {
|
|
||||||
try {
|
|
||||||
final stat = entity.statSync();
|
|
||||||
final fileName = entity.path.split('/').last;
|
|
||||||
sessions.add(_SessionFile(
|
|
||||||
file: entity,
|
|
||||||
stat: stat,
|
|
||||||
sessionId: fileName.replaceAll('.jsonl', ''),
|
|
||||||
));
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessions.isEmpty) continue;
|
if (sessions.isEmpty) continue;
|
||||||
|
|
||||||
sessions.sort((a, b) => b.stat.modified.compareTo(a.stat.modified));
|
sessions.sort((a, b) => b.stat.modified.compareTo(a.stat.modified));
|
||||||
@ -283,7 +291,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickFile() async {
|
Future<void> _pickFile() async {
|
||||||
final home = Platform.environment['HOME'] ?? '';
|
final home = _getRealHome() ?? '';
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: ['jsonl'],
|
allowedExtensions: ['jsonl'],
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class Sidebar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Text(
|
const Text(
|
||||||
'Session Viewer',
|
'Session Analysis',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -107,7 +107,7 @@ class Sidebar extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Claude Session Viewer v0.1',
|
'Claude Session Analysis v1.0',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: AppColors.textMuted.withAlpha(128),
|
color: AppColors.textMuted.withAlpha(128),
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
platform :osx, '10.15'
|
platform :osx, '13.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|||||||
@ -1,29 +1,16 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- file_picker (0.0.1):
|
|
||||||
- FlutterMacOS
|
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- shared_preferences_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
file_picker:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
shared_preferences_foundation:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
PODFILE CHECKSUM: 89c84cf5c2351c1e554c6dea18d31a879fc3a19e
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
|
33CC10FF2044A3C60003C045 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10FE2044A3C60003C045 /* PrivacyInfo.xcprivacy */; };
|
||||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
79F3DEC2140214566E19F388 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6CB8C54BF040E1E6BF05BCBD /* Pods_RunnerTests.framework */; };
|
79F3DEC2140214566E19F388 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6CB8C54BF040E1E6BF05BCBD /* Pods_RunnerTests.framework */; };
|
||||||
@ -68,7 +69,7 @@
|
|||||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||||
33CC10ED2044A3C60003C045 /* claude_session_viewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = claude_session_viewer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
33CC10ED2044A3C60003C045 /* Claude Session Analysis.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Claude Session Analysis.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
@ -77,6 +78,7 @@
|
|||||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
33CC10FE2044A3C60003C045 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||||
@ -147,7 +149,7 @@
|
|||||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
33CC10ED2044A3C60003C045 /* claude_session_viewer.app */,
|
33CC10ED2044A3C60003C045 /* Claude Session Analysis.app */,
|
||||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
@ -181,6 +183,7 @@
|
|||||||
children = (
|
children = (
|
||||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||||
|
33CC10FE2044A3C60003C045 /* PrivacyInfo.xcprivacy */,
|
||||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||||
33E51914231749380026EE4D /* Release.entitlements */,
|
33E51914231749380026EE4D /* Release.entitlements */,
|
||||||
33CC11242044D66E0003C045 /* Resources */,
|
33CC11242044D66E0003C045 /* Resources */,
|
||||||
@ -244,7 +247,6 @@
|
|||||||
33CC10EB2044A3C60003C045 /* Resources */,
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
2C302F33045D329C15CB5562 /* [CP] Embed Pods Frameworks */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -256,7 +258,7 @@
|
|||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
);
|
);
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 33CC10ED2044A3C60003C045 /* claude_session_viewer.app */;
|
productReference = 33CC10ED2044A3C60003C045 /* Claude Session Analysis.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
@ -327,29 +329,13 @@
|
|||||||
files = (
|
files = (
|
||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||||
|
33CC10FF2044A3C60003C045 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
2C302F33045D329C15CB5562 /* [CP] Embed Pods Frameworks */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
|
||||||
outputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
2D5E86AC9DD4210FFD8C9DE8 /* [CP] Check Pods Manifest.lock */ = {
|
2D5E86AC9DD4210FFD8C9DE8 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -489,10 +475,10 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.svrnty.claudeSessionViewer.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.svrnty.claudeSessionAnalysis.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/claude_session_viewer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/claude_session_viewer";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Claude Session Analysis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Claude Session Analysis";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -504,10 +490,10 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.svrnty.claudeSessionViewer.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.svrnty.claudeSessionAnalysis.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/claude_session_viewer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/claude_session_viewer";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Claude Session Analysis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Claude Session Analysis";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@ -519,10 +505,10 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.svrnty.claudeSessionViewer.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.svrnty.claudeSessionAnalysis.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/claude_session_viewer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/claude_session_viewer";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Claude Session Analysis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Claude Session Analysis";
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
@ -567,7 +553,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@ -649,7 +635,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@ -699,7 +685,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
// 'flutter create' template.
|
// 'flutter create' template.
|
||||||
|
|
||||||
// The application's name. By default this is also the title of the Flutter window.
|
// The application's name. By default this is also the title of the Flutter window.
|
||||||
PRODUCT_NAME = claude_session_viewer
|
PRODUCT_NAME = Claude Session Analysis
|
||||||
|
|
||||||
// The application's bundle identifier
|
// The application's bundle identifier
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.svrnty.claudeSessionViewer
|
PRODUCT_BUNDLE_IDENTIFIER = com.svrnty.claudeSessionAnalysis
|
||||||
|
|
||||||
// The copyright displayed in application information
|
// The copyright displayed in application information
|
||||||
PRODUCT_COPYRIGHT = Copyright © 2026 com.svrnty. All rights reserved.
|
PRODUCT_COPYRIGHT = Copyright © 2026 Svrnty. All rights reserved.
|
||||||
|
|||||||
@ -22,6 +22,8 @@
|
|||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.developer-tools</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||||
<key>NSMainNibFile</key>
|
<key>NSMainNibFile</key>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class MainFlutterWindow: NSWindow {
|
|||||||
self.contentViewController = flutterViewController
|
self.contentViewController = flutterViewController
|
||||||
self.setFrame(windowFrame, display: true)
|
self.setFrame(windowFrame, display: true)
|
||||||
self.minSize = NSSize(width: 1200, height: 700)
|
self.minSize = NSSize(width: 1200, height: 700)
|
||||||
self.title = "Claude Session Viewer"
|
self.title = "Claude Session Analysis"
|
||||||
|
|
||||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||||
|
|
||||||
|
|||||||
31
macos/Runner/PrivacyInfo.xcprivacy
Normal file
31
macos/Runner/PrivacyInfo.xcprivacy
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
16
pubspec.lock
16
pubspec.lock
@ -29,10 +29,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -276,18 +276,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -521,10 +521,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.10"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: claude_session_viewer
|
name: claude_session_analysis
|
||||||
description: "A new Flutter project."
|
description: "Analyze and explore Claude Code session transcripts with rich visualizations, token usage tracking, and agent timeline views."
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:claude_session_viewer/main.dart';
|
import 'package:claude_session_analysis/main.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('App launches', (WidgetTester tester) async {
|
testWidgets('App launches', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const ClaudeSessionViewerApp());
|
await tester.pumpWidget(const ClaudeSessionAnalysisApp());
|
||||||
expect(find.text('Claude Session Viewer'), findsOneWidget);
|
expect(find.text('Claude Session Analysis'), findsOneWidget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user